From a883352f79db71b6ec81d70840dec41f28264df7 Mon Sep 17 00:00:00 2001 From: roybarda Date: Wed, 26 Oct 2022 19:33:19 +0300 Subject: [PATCH] First release of open-appsec source code --- CMakeLists.txt | 38 + CODE_OF_CONDUCT.md | 47 + CONTRIBUTING.md | 37 + ...014-Report-Code_audit-OPEN-APPSEC-v1.2.pdf | Bin 0 -> 978493 bytes LICENSE | 201 + README.md | 141 +- SECURITY.md | 7 + attachments/CMakeLists.txt | 1 + .../core/include/common_is/kdebug_flags.h | 34 + attachments/nginx/CMakeLists.txt | 1 + .../nginx_attachment_util/CMakeLists.txt | 8 + .../nginx_attachment_util.cc | 251 + .../nginx_attachment_util_ut/CMakeLists.txt | 9 + .../nginx_attachment_util_ut.cc | 125 + build_system/CMakeLists.txt | 2 + build_system/charts/CMakeLists.txt | 6 + .../open-appsec-k8s-nginx-ingress/.helmignore | 22 + .../CHANGELOG.md | 375 + .../open-appsec-k8s-nginx-ingress/Chart.yaml | 22 + .../open-appsec-k8s-nginx-ingress/OWNERS | 10 + .../open-appsec-k8s-nginx-ingress/README.md | 487 + .../README.md.gotmpl | 235 + .../controller-custom-ingressclass-flags.yaml | 7 + .../ci/daemonset-customconfig-values.yaml | 10 + .../ci/daemonset-customnodeport-values.yaml | 18 + .../ci/daemonset-extra-modules.yaml | 7 + .../ci/daemonset-headers-values.yaml | 10 + .../ci/daemonset-internal-lb-values.yaml | 10 + .../ci/daemonset-nodeport-values.yaml | 6 + .../ci/daemonset-podannotations-values.yaml | 13 + ...set-tcp-udp-configMapNamespace-values.yaml | 16 + ...emonset-tcp-udp-portNamePrefix-values.yaml | 14 + .../ci/daemonset-tcp-udp-values.yaml | 12 + .../ci/daemonset-tcp-values.yaml | 10 + .../ci/deamonset-default-values.yaml | 6 + .../ci/deamonset-metrics-values.yaml | 8 + .../ci/deamonset-psp-values.yaml | 9 + .../ci/deamonset-webhook-and-psp-values.yaml | 9 + .../ci/deamonset-webhook-values.yaml | 6 + ...eployment-autoscaling-behavior-values.yaml | 14 + .../ci/deployment-autoscaling-values.yaml | 7 + .../ci/deployment-customconfig-values.yaml | 8 + .../ci/deployment-customnodeport-values.yaml | 16 + .../ci/deployment-default-values.yaml | 4 + .../ci/deployment-extra-modules.yaml | 6 + .../ci/deployment-headers-values.yaml | 9 + .../ci/deployment-internal-lb-values.yaml | 9 + .../ci/deployment-metrics-values.yaml | 7 + .../ci/deployment-nodeport-values.yaml | 5 + .../ci/deployment-podannotations-values.yaml | 12 + .../ci/deployment-psp-values.yaml | 6 + ...ent-tcp-udp-configMapNamespace-values.yaml | 15 + ...loyment-tcp-udp-portNamePrefix-values.yaml | 13 + .../ci/deployment-tcp-udp-values.yaml | 11 + .../ci/deployment-tcp-values.yaml | 7 + .../ci/deployment-webhook-and-psp-values.yaml | 8 + .../deployment-webhook-resources-values.yaml | 23 + .../ci/deployment-webhook-values.yaml | 5 + .../crds/crd-openappsec-custom-response.yaml | 40 + .../crds/crd-openappsec-exception.yaml | 75 + .../crds/crd-openappsec-log-trigger.yaml | 107 + .../crds/crd-openappsec-policy.yaml | 90 + .../crds/crd-openappsec-practice.yaml | 135 + .../crd-openappsec-sources-identifier.yaml | 40 + .../crds/crd-openappsec-trusted-sources.yaml | 32 + .../templates/_helpers.tpl | 185 + .../templates/_params.tpl | 62 + .../job-patch/clusterrole.yaml | 34 + .../job-patch/clusterrolebinding.yaml | 23 + .../job-patch/job-createSecret.yaml | 76 + .../job-patch/job-patchWebhook.yaml | 78 + .../admission-webhooks/job-patch/psp.yaml | 39 + .../admission-webhooks/job-patch/role.yaml | 24 + .../job-patch/rolebinding.yaml | 24 + .../job-patch/serviceaccount.yaml | 16 + .../validating-webhook.yaml | 48 + .../templates/appsec-learning-pvc.yaml | 20 + .../templates/appsec-pvc.yaml | 51 + .../templates/clusterrole.yaml | 95 + .../templates/clusterrolebinding.yaml | 19 + .../controller-configmap-addheaders.yaml | 14 + .../controller-configmap-proxyheaders.yaml | 19 + .../templates/controller-configmap-tcp.yaml | 17 + .../templates/controller-configmap-udp.yaml | 17 + .../templates/controller-configmap.yaml | 29 + .../templates/controller-daemonset.yaml | 223 + .../templates/controller-deployment.yaml | 261 + .../templates/controller-hpa.yaml | 52 + .../templates/controller-ingressclass.yaml | 21 + .../templates/controller-keda.yaml | 42 + .../controller-poddisruptionbudget.yaml | 19 + .../templates/controller-prometheusrules.yaml | 21 + .../templates/controller-psp.yaml | 92 + .../templates/controller-role.yaml | 101 + .../templates/controller-rolebinding.yaml | 21 + .../controller-service-internal.yaml | 79 + .../templates/controller-service-metrics.yaml | 45 + .../templates/controller-service-webhook.yaml | 40 + .../templates/controller-service.yaml | 101 + .../templates/controller-serviceaccount.yaml | 18 + .../templates/controller-servicemonitor.yaml | 48 + .../templates/controller-statefulset.yaml | 304 + .../templates/default-backend-deployment.yaml | 118 + .../templates/default-backend-hpa.yaml | 33 + .../default-backend-poddisruptionbudget.yaml | 21 + .../templates/default-backend-psp.yaml | 36 + .../templates/default-backend-role.yaml | 22 + .../default-backend-rolebinding.yaml | 21 + .../templates/default-backend-service.yaml | 41 + .../default-backend-serviceaccount.yaml | 14 + .../templates/default-policy.yaml | 49 + .../templates/dh-param-secret.yaml | 10 + .../templates/learning-deployment.yaml | 139 + .../templates/learning-services.yaml | 33 + .../open-appsec-k8s-nginx-ingress/values.yaml | 1009 + build_system/docker/CMakeLists.txt | 9 + build_system/docker/Dockerfile | 23 + build_system/docker/entry.sh | 81 + .../tools/packaging/makeself_wrapper.sh | 17 + components/CMakeLists.txt | 14 + components/attachment-intakers/CMakeLists.txt | 2 + .../attachment_registrator/CMakeLists.txt | 1 + .../attachment_registrator.cc | 470 + .../nginx_attachment/CMakeLists.txt | 5 + .../nginx_attachment/cidrs_data.cc | 128 + .../nginx_attachment/cidrs_data.h | 28 + .../nginx_attachment/intentional_failure.cc | 145 + .../nginx_attachment/intentional_failure.h | 56 + .../nginx_attachment/nginx_attachment.cc | 1785 + .../nginx_attachment_config.cc | 331 + .../nginx_attachment_config.h | 77 + .../nginx_attachment_metric.cc | 163 + .../nginx_attachment_opaque.cc | 121 + .../nginx_attachment_opaque.h | 94 + .../nginx_attachment/nginx_intaker_metric.cc | 502 + .../nginx_attachment/nginx_parser.cc | 369 + .../nginx_attachment/nginx_parser.h | 45 + .../user_identifiers_config.cc | 447 + components/generic_rulebase/CMakeLists.txt | 4 + components/generic_rulebase/assets_config.cc | 137 + .../evaluators/CMakeLists.txt | 1 + .../generic_rulebase/evaluators/asset_eval.cc | 40 + .../evaluators/connection_eval.cc | 299 + .../evaluators/http_transaction_data_eval.cc | 125 + .../evaluators/parameter_eval.cc | 38 + .../evaluators/practice_eval.cc | 50 + .../generic_rulebase/evaluators/query_eval.cc | 136 + .../evaluators/trigger_eval.cc | 57 + .../generic_rulebase/evaluators/zone_eval.cc | 44 + .../generic_rulebase/generic_rulebase.cc | 125 + .../generic_rulebase_context.cc | 109 + components/generic_rulebase/match_query.cc | 291 + .../generic_rulebase/parameters_config.cc | 126 + .../generic_rulebase/rulebase_config.cc | 79 + .../generic_rulebase/triggers_config.cc | 216 + components/generic_rulebase/zone.cc | 179 + components/generic_rulebase/zones_config.cc | 114 + components/gradual_deployment/CMakeLists.txt | 7 + .../gradual_deployment/gradual_deployment.cc | 227 + .../gradual_deployment_ut/CMakeLists.txt | 8 + .../gradual_deployment_ut.cc | 127 + .../health_check_manager/CMakeLists.txt | 3 + .../health_check_manager.cc | 250 + .../health_check_manager_ut/CMakeLists.txt | 8 + .../health_check_manager_ut.cc | 219 + components/http_manager/CMakeLists.txt | 1 + components/http_manager/http_manager.cc | 379 + .../http_manager/http_manager_opaque.cc | 103 + components/http_manager/http_manager_opaque.h | 55 + .../http_transaction_data/CMakeLists.txt | 5 + .../http_transaction_data.cc | 265 + .../http_transaction_data_ut/CMakeLists.txt | 7 + .../http_transaction_data_ut.cc | 127 + components/include/WaapEnums.h | 71 + components/include/attachment_registrator.h | 47 + components/include/byteorder.h | 48 + components/include/details_resolver.h | 43 + components/include/downloader.h | 50 + components/include/external_sdk_server.h | 43 + components/include/generic_rulebase/asset.h | 31 + .../include/generic_rulebase/assets_config.h | 91 + .../generic_rulebase/evaluators/asset_eval.h | 36 + .../evaluators/connection_eval.h | 127 + .../evaluators/http_transaction_data_eval.h | 74 + .../evaluators/parameter_eval.h | 36 + .../evaluators/practice_eval.h | 36 + .../generic_rulebase/evaluators/query_eval.h | 43 + .../evaluators/trigger_eval.h | 36 + .../generic_rulebase/evaluators/zone_eval.h | 36 + .../generic_rulebase/generic_rulebase.h | 44 + .../generic_rulebase_context.h | 39 + .../generic_rulebase/generic_rulebase_utils.h | 39 + .../include/generic_rulebase/match_query.h | 93 + .../generic_rulebase/parameters_config.h | 221 + .../generic_rulebase/rulebase_config.h | 167 + .../generic_rulebase/triggers_config.h | 174 + components/include/generic_rulebase/zone.h | 55 + .../include/generic_rulebase/zones_config.h | 53 + components/include/gradual_deployment.h | 43 + components/include/health_check_manager.h | 45 + components/include/health_checker.h | 44 + .../include/http_event_impl/filter_verdict.h | 69 + .../http_event_impl/i_http_event_impl.h | 387 + components/include/http_inspection_events.h | 186 + components/include/http_manager.h | 47 + components/include/http_transaction_common.h | 37 + components/include/http_transaction_data.h | 136 + components/include/hybrid_mode_telemetry.h | 34 + components/include/i_details_resolver.h | 42 + components/include/i_downloader.h | 44 + components/include/i_external_sdk_server.h | 60 + components/include/i_generic_rulebase.h | 36 + components/include/i_gradual_deployment.h | 37 + components/include/i_http_manager.h | 34 + components/include/i_k8s_policy_gen.h | 27 + components/include/i_manifest_controller.h | 26 + components/include/i_orchestration_status.h | 87 + components/include/i_orchestration_tools.h | 125 + components/include/i_package_handler.h | 47 + components/include/i_pm_scan.h | 67 + components/include/i_service_controller.h | 62 + .../include/i_static_resources_handler.h | 30 + components/include/i_update_communication.h | 36 + components/include/i_waap_telemetry.h | 46 + components/include/ip_utilities.h | 108 + components/include/k8s_policy_gen.h | 44 + components/include/manifest_controller.h | 53 + components/include/manifest_diff_calculator.h | 50 + components/include/manifest_handler.h | 76 + .../include/messaging_downloader_client.h | 49 + .../include/messaging_downloader_server.h | 51 + .../include/mock/mock_nginx_attachment.h | 16 + components/include/nginx_attachment.h | 57 + components/include/nginx_attachment_metric.h | 87 + components/include/nginx_intaker_metric.h | 146 + components/include/orchestration_comp.h | 73 + components/include/orchestration_status.h | 49 + components/include/orchestration_tools.h | 33 + components/include/orchestrator/data.h | 46 + .../orchestrator/rest_api/get_resource_file.h | 123 + .../rest_api/orchestration_check_update.h | 182 + components/include/package.h | 75 + components/include/package_handler.h | 42 + components/include/packet.h | 189 + components/include/pending_key.h | 85 + components/include/pm_hook.h | 45 + components/include/report_messaging.h | 150 + components/include/service_controller.h | 55 + components/include/service_details.h | 79 + components/include/signal_handler.h | 50 + components/include/telemetry.h | 122 + components/include/transaction_table_metric.h | 61 + components/include/type_defs.h | 7 + components/include/update_communication.h | 46 + components/include/url_parser.h | 56 + components/include/user_identifiers_config.h | 66 + components/include/waap.h | 66 + .../messaging_downloader/CMakeLists.txt | 4 + .../messaging_downloader_client.cc | 230 + .../messaging_downloader_server.cc | 375 + .../messaging_downloader_ut/CMakeLists.txt | 2 + .../downloader_client_ut/CMakeLists.txt | 9 + .../downloader_client_ut.cc | 113 + .../downloader_server_ut/CMakeLists.txt | 9 + .../downloader_server_ut.cc | 304 + components/packet/CMakeLists.txt | 2 + components/packet/packet.cc | 607 + components/packet/packet_ut/CMakeLists.txt | 7 + components/packet/packet_ut/packet_ut.cc | 844 + components/pending_key/CMakeLists.txt | 2 + components/pending_key/pending_key.cc | 54 + .../pending_key/pending_key_ut/CMakeLists.txt | 5 + .../pending_key_ut/pending_key_ut.cc | 143 + components/report_messaging/CMakeLists.txt | 3 + .../report_messaging/report_messaging.cc | 47 + .../report_messaging_ut/CMakeLists.txt | 3 + .../report_messaging_ut.cc | 334 + components/security_apps/CMakeLists.txt | 2 + .../orchestration/CMakeLists.txt | 17 + .../details_resolver/CMakeLists.txt | 3 + .../details_resolver/details_resolver.cc | 288 + .../checkpoint_product_handlers.h | 183 + .../details_resolver_impl.h | 72 + .../details_resolving_handler.cc | 128 + .../details_resolving_handler.h | 41 + .../orchestration/downloader/CMakeLists.txt | 5 + .../orchestration/downloader/curl_client.cc | 438 + .../orchestration/downloader/curl_client.h | 115 + .../orchestration/downloader/downloader.cc | 387 + .../downloader/downloader_ut/CMakeLists.txt | 7 + .../downloader/downloader_ut/downloader_ut.cc | 431 + .../orchestration/downloader/http_client.cc | 276 + .../orchestration/downloader/http_client.h | 39 + .../orchestration/downloader/https_client.cc | 619 + .../orchestration/health_check/CMakeLists.txt | 3 + .../health_check/health_check.cc | 340 + .../health_check_ut/CMakeLists.txt | 7 + .../health_check_ut/health_check_ut.cc | 260 + .../orchestration/hybrid_mode_telemetry.cc | 59 + .../orchestration/include/fog_authenticator.h | 301 + .../orchestration/include/fog_communication.h | 45 + .../orchestration/include/get_status_rest.h | 91 + .../include/hybrid_communication.h | 58 + .../include/local_communication.h | 43 + .../include/mock/mock_details_resolver.h | 46 + .../include/mock/mock_downloader.h | 42 + .../include/mock/mock_manifest_controller.h | 28 + .../include/mock/mock_messaging_downloader.h | 39 + .../include/mock/mock_orchestration_status.h | 63 + .../include/mock/mock_orchestration_tools.h | 58 + .../include/mock/mock_package_handler.h | 31 + .../include/mock/mock_service_controller.h | 62 + .../include/mock/mock_update_communication.h | 37 + .../include/orchestration_policy.h | 38 + .../k8s_policy_gen/CMakeLists.txt | 1 + .../include/appsec_practice_section.h | 768 + .../include/exceptions_section.h | 313 + .../k8s_policy_gen/include/ingress_data.h | 224 + .../include/k8s_policy_common.h | 103 + .../include/rules_config_section.h | 391 + .../k8s_policy_gen/include/settings_section.h | 121 + .../k8s_policy_gen/include/snort_section.h | 79 + .../k8s_policy_gen/include/triggers_section.h | 625 + .../include/trusted_sources_section.h | 186 + .../k8s_policy_gen/k8s_policy_gen.cc | 1167 + .../manifest_controller/CMakeLists.txt | 3 + .../manifest_controller.cc | 445 + .../manifest_controller_ut/CMakeLists.txt | 7 + .../manifest_controller_ut.cc | 2431 ++ .../manifest_diff_calculator.cc | 144 + .../manifest_controller/manifest_handler.cc | 384 + .../orchestration/modules/CMakeLists.txt | 10 + .../orchestration/modules/data.cc | 52 + .../modules/modules_ut/CMakeLists.txt | 7 + .../modules/modules_ut/data_ut.cc | 85 + .../modules_ut/orchestration_policy_ut.cc | 158 + .../modules_ut/orchestration_status_ut.cc | 484 + .../modules/modules_ut/package_ut.cc | 236 + .../modules/modules_ut/url_parser_ut.cc | 117 + .../modules/orchestration_policy.cc | 64 + .../modules/orchestration_status.cc | 685 + .../orchestration/modules/package.cc | 133 + .../orchestration/modules/url_parser.cc | 149 + .../orchestration/orchestration_comp.cc | 1629 + .../orchestration_tools/CMakeLists.txt | 5 + .../orchestration_tools.cc | 475 + .../orchestration_tools_ut/CMakeLists.txt | 7 + .../orchestration_tools_ut.cc | 263 + .../orchestration_ut/CMakeLists.txt | 15 + .../orchestration_multitenant_ut.cc | 445 + .../orchestration_ut/orchestration_ut.cc | 1705 + .../package_handler/CMakeLists.txt | 3 + .../package_handler/package_handler.cc | 508 + .../package_handler_ut/CMakeLists.txt | 8 + .../package_handler_ut/package_handler_ut.cc | 404 + .../service_controller/CMakeLists.txt | 3 + .../service_controller/service_controller.cc | 937 + .../service_controller_ut/CMakeLists.txt | 7 + .../service_controller_ut.cc | 1604 + .../update_communication/CMakeLists.txt | 3 + .../update_communication/fog_authenticator.cc | 572 + .../update_communication/fog_communication.cc | 89 + .../hybrid_communication.cc | 128 + .../local_communication.cc | 187 + .../update_communication.cc | 148 + .../update_communication_ut/CMakeLists.txt | 7 + .../local_communication_ut.cc | 233 + components/security_apps/waap/CMakeLists.txt | 13 + .../waap/first_request_object.cc | 57 + .../security_apps/waap/first_request_object.h | 42 + .../security_apps/waap/include/WaapDefines.h | 30 + .../waap/include/i_deepAnalyzer.h | 24 + .../waap/include/i_ignoreSources.h | 26 + .../waap/include/i_indicatorsFilter.h | 39 + .../security_apps/waap/include/i_serialize.h | 281 + .../waap/include/i_transaction.h | 144 + .../security_apps/waap/include/i_waapConfig.h | 69 + .../waap/include/i_waap_asset_state.h | 27 + .../security_apps/waap/include/picojson.h | 1178 + .../waap/include/reputation_features_events.h | 100 + .../waap/reputation/CMakeLists.txt | 3 + .../reputation/reputation_features_agg.cc | 379 + .../waap/reputation/reputation_features_agg.h | 221 + .../security_apps/waap/resources/1.data | 26379 ++++++++++++ .../security_apps/waap/resources/2.data | 33434 ++++++++++++++++ .../security_apps/waap/resources/8.data | 87 + .../waap_clib/AutonomousSecurityDecision.cc | 94 + .../waap_clib/AutonomousSecurityDecision.h | 53 + .../waap/waap_clib/BehaviorAnalysis.cc | 442 + .../waap/waap_clib/BehaviorAnalysis.h | 179 + .../waap/waap_clib/CMakeLists.txt | 108 + .../security_apps/waap/waap_clib/CidrMatch.cc | 159 + .../security_apps/waap/waap_clib/CidrMatch.h | 39 + .../waap/waap_clib/ConfidenceCalculator.cc | 738 + .../waap/waap_clib/ConfidenceCalculator.h | 155 + .../waap/waap_clib/ConfidenceFile.cc | 37 + .../waap/waap_clib/ConfidenceFile.h | 41 + .../waap/waap_clib/ContentTypeParser.cc | 75 + .../waap/waap_clib/ContentTypeParser.h | 49 + .../security_apps/waap/waap_clib/Csrf.cc | 111 + .../security_apps/waap/waap_clib/Csrf.h | 47 + .../waap/waap_clib/CsrfDecision.cc | 22 + .../waap/waap_clib/CsrfDecision.h | 27 + .../waap/waap_clib/CsrfPolicy.cc | 35 + .../security_apps/waap/waap_clib/CsrfPolicy.h | 52 + .../security_apps/waap/waap_clib/D2Main.cc | 73 + .../security_apps/waap/waap_clib/D2Main.h | 64 + .../security_apps/waap/waap_clib/DataTypes.h | 28 + .../waap/waap_clib/DecisionFactory.cc | 147 + .../waap/waap_clib/DecisionFactory.h | 45 + .../waap/waap_clib/DecisionType.h | 31 + .../waap/waap_clib/DeepAnalyzer.cc | 160 + .../waap/waap_clib/DeepAnalyzer.h | 71 + .../waap/waap_clib/DeepParser.cc | 1068 + .../security_apps/waap/waap_clib/DeepParser.h | 140 + .../waap/waap_clib/ErrorDisclosureDecision.cc | 22 + .../waap/waap_clib/ErrorDisclosureDecision.h | 28 + .../waap/waap_clib/ErrorLimiting.cc | 59 + .../waap/waap_clib/ErrorLimiting.h | 95 + .../waap/waap_clib/ErrorLimitingDecision.cc | 22 + .../waap/waap_clib/ErrorLimitingDecision.h | 27 + .../waap/waap_clib/FpMitigation.cc | 226 + .../waap/waap_clib/FpMitigation.h | 90 + .../waap/waap_clib/IndicatorsFilterBase.cc | 193 + .../waap/waap_clib/IndicatorsFilterBase.h | 56 + .../waap_clib/IndicatorsFiltersManager.cc | 317 + .../waap/waap_clib/IndicatorsFiltersManager.h | 70 + .../security_apps/waap/waap_clib/KeyStack.cc | 60 + .../security_apps/waap/waap_clib/KeyStack.h | 81 + .../waap/waap_clib/KeywordIndicatorFilter.cc | 125 + .../waap/waap_clib/KeywordIndicatorFilter.h | 48 + .../waap/waap_clib/KeywordTypeValidator.cc | 81 + .../waap/waap_clib/KeywordTypeValidator.h | 35 + .../waap/waap_clib/LogGenWrapper.cc | 63 + .../waap/waap_clib/LogGenWrapper.h | 46 + .../waap/waap_clib/OpenRedirectDecision.cc | 34 + .../waap/waap_clib/OpenRedirectDecision.h | 34 + .../waap/waap_clib/PHPSerializedDataParser.cc | 804 + .../waap/waap_clib/PHPSerializedDataParser.h | 89 + .../waap/waap_clib/ParserBase.cc | 88 + .../security_apps/waap/waap_clib/ParserBase.h | 141 + .../waap/waap_clib/ParserBinary.cc | 157 + .../waap/waap_clib/ParserBinary.h | 46 + .../waap/waap_clib/ParserConfluence.cc | 180 + .../waap/waap_clib/ParserConfluence.h | 48 + .../waap/waap_clib/ParserDelimiter.cc | 140 + .../waap/waap_clib/ParserDelimiter.h | 51 + .../waap/waap_clib/ParserHTML.cc | 304 + .../security_apps/waap/waap_clib/ParserHTML.h | 84 + .../waap/waap_clib/ParserHdrValue.cc | 448 + .../waap/waap_clib/ParserHdrValue.h | 58 + .../waap/waap_clib/ParserJson.cc | 342 + .../security_apps/waap/waap_clib/ParserJson.h | 87 + .../waap/waap_clib/ParserMultipartForm.cc | 485 + .../waap/waap_clib/ParserMultipartForm.h | 93 + .../security_apps/waap/waap_clib/ParserRaw.cc | 80 + .../security_apps/waap/waap_clib/ParserRaw.h | 43 + .../waap/waap_clib/ParserUrlEncode.cc | 439 + .../waap/waap_clib/ParserUrlEncode.h | 57 + .../security_apps/waap/waap_clib/ParserXML.cc | 334 + .../security_apps/waap/waap_clib/ParserXML.h | 101 + .../waap/waap_clib/PatternMatcher.cc | 53 + .../waap/waap_clib/PatternMatcher.h | 47 + .../waap/waap_clib/RateLimiter.cc | 80 + .../waap/waap_clib/RateLimiter.h | 42 + .../waap/waap_clib/RateLimiting.cc | 255 + .../waap/waap_clib/RateLimiting.h | 337 + .../waap/waap_clib/RateLimitingDecision.cc | 22 + .../waap/waap_clib/RateLimitingDecision.h | 27 + .../waap/waap_clib/ScanResult.cc | 79 + .../security_apps/waap/waap_clib/ScanResult.h | 41 + .../waap/waap_clib/ScannerDetector.cc | 235 + .../waap/waap_clib/ScannersDetector.h | 55 + .../waap/waap_clib/ScoreBuilder.cc | 499 + .../waap/waap_clib/ScoreBuilder.h | 173 + .../waap/waap_clib/SecurityHeadersPolicy.cc | 104 + .../waap/waap_clib/SecurityHeadersPolicy.h | 225 + .../waap/waap_clib/Serializator.cc | 850 + .../waap/waap_clib/Signatures.cc | 278 + .../security_apps/waap/waap_clib/Signatures.h | 93 + .../waap/waap_clib/SingleDecision.cc | 53 + .../waap/waap_clib/SingleDecision.h | 39 + .../waap_clib/SyncLearningNotification.cc | 58 + .../waap/waap_clib/SyncLearningNotification.h | 59 + .../security_apps/waap/waap_clib/Telemetry.cc | 304 + .../waap/waap_clib/TrustedSources.cc | 217 + .../waap/waap_clib/TrustedSources.h | 111 + .../waap_clib/TrustedSourcesConfidence.cc | 269 + .../waap/waap_clib/TrustedSourcesConfidence.h | 57 + .../waap/waap_clib/TuningDecision.cc | 158 + .../waap/waap_clib/TuningDecisions.h | 89 + .../waap/waap_clib/TypeIndicatorsFilter.cc | 152 + .../waap/waap_clib/TypeIndicatorsFilter.h | 52 + .../waap/waap_clib/UserLimitsDecision.cc | 22 + .../waap/waap_clib/UserLimitsDecision.h | 27 + .../waap/waap_clib/UserLimitsPolicy.cc | 330 + .../waap/waap_clib/UserLimitsPolicy.h | 183 + .../waap/waap_clib/WaapAssetState.cc | 1965 + .../waap/waap_clib/WaapAssetState.h | 171 + .../waap/waap_clib/WaapAssetStatesManager.cc | 187 + .../waap/waap_clib/WaapAssetStatesManager.h | 71 + .../waap/waap_clib/WaapConfigApi.cc | 114 + .../waap/waap_clib/WaapConfigApi.h | 60 + .../waap/waap_clib/WaapConfigApplication.cc | 87 + .../waap/waap_clib/WaapConfigApplication.h | 57 + .../waap/waap_clib/WaapConfigBase.cc | 450 + .../waap/waap_clib/WaapConfigBase.h | 107 + .../waap/waap_clib/WaapConversions.cc | 73 + .../waap/waap_clib/WaapConversions.h | 27 + .../waap/waap_clib/WaapDecision.cc | 208 + .../waap/waap_clib/WaapDecision.h | 61 + .../waap_clib/WaapErrorDisclosurePolicy.cc | 27 + .../waap_clib/WaapErrorDisclosurePolicy.h | 50 + .../waap/waap_clib/WaapKeywords.cc | 47 + .../waap/waap_clib/WaapKeywords.h | 31 + .../waap/waap_clib/WaapOpenRedirect.cc | 90 + .../waap/waap_clib/WaapOpenRedirect.h | 34 + .../waap/waap_clib/WaapOpenRedirectPolicy.cc | 36 + .../waap/waap_clib/WaapOpenRedirectPolicy.h | 52 + .../waap/waap_clib/WaapOverride.cc | 81 + .../waap/waap_clib/WaapOverride.h | 345 + .../waap/waap_clib/WaapOverrideFunctor.cc | 108 + .../waap/waap_clib/WaapOverrideFunctor.h | 35 + .../waap/waap_clib/WaapParameters.cc | 35 + .../waap/waap_clib/WaapParameters.h | 45 + .../waap/waap_clib/WaapRegexPreconditions.cc | 341 + .../waap/waap_clib/WaapRegexPreconditions.h | 89 + .../waap_clib/WaapResponseInjectReasons.cc | 91 + .../waap_clib/WaapResponseInjectReasons.h | 35 + .../waap_clib/WaapResponseInspectReasons.cc | 79 + .../waap_clib/WaapResponseInspectReasons.h | 35 + .../waap/waap_clib/WaapResultJson.cc | 255 + .../waap/waap_clib/WaapResultJson.h | 21 + .../waap/waap_clib/WaapSampleValue.cc | 41 + .../waap/waap_clib/WaapSampleValue.h | 37 + .../waap/waap_clib/WaapScanner.cc | 307 + .../waap/waap_clib/WaapScanner.h | 52 + .../waap/waap_clib/WaapScores.cc | 113 + .../security_apps/waap/waap_clib/WaapScores.h | 53 + .../waap/waap_clib/WaapTrigger.cc | 79 + .../waap/waap_clib/WaapTrigger.h | 156 + .../waap/waap_clib/WaapValueStatsAnalyzer.cc | 264 + .../waap/waap_clib/WaapValueStatsAnalyzer.h | 39 + .../waap/waap_clib/Waf2Engine.cc | 2271 ++ .../security_apps/waap/waap_clib/Waf2Engine.h | 352 + .../waap/waap_clib/Waf2EngineGetters.cc | 623 + .../security_apps/waap/waap_clib/Waf2Regex.cc | 653 + .../security_apps/waap/waap_clib/Waf2Regex.h | 100 + .../security_apps/waap/waap_clib/Waf2Util.cc | 1921 + .../security_apps/waap/waap_clib/Waf2Util.h | 1154 + .../waap/waap_clib/lru_cache_map.h | 113 + .../waap/waap_clib/lru_cache_set.h | 98 + .../waap/waap_clib/waf2_reporting.h | 198 + .../security_apps/waap/waap_component.cc | 73 + .../security_apps/waap/waap_component_impl.cc | 750 + .../security_apps/waap/waap_component_impl.h | 88 + components/signal_handler/CMakeLists.txt | 2 + components/signal_handler/signal_handler.cc | 479 + components/utils/CMakeLists.txt | 2 + components/utils/ip_utilities/CMakeLists.txt | 1 + components/utils/ip_utilities/ip_utilities.cc | 347 + components/utils/pm/CMakeLists.txt | 3 + components/utils/pm/debugpm.cc | 63 + components/utils/pm/debugpm.h | 39 + components/utils/pm/general_adaptor.cc | 65 + components/utils/pm/general_adaptor.h | 80 + components/utils/pm/kiss_hash.cc | 1783 + components/utils/pm/kiss_hash.h | 586 + components/utils/pm/kiss_patterns.cc | 134 + components/utils/pm/kiss_patterns.h | 74 + components/utils/pm/kiss_pm_stats.cc | 429 + components/utils/pm/kiss_pm_stats.h | 146 + components/utils/pm/kiss_thin_nfa.cc | 462 + components/utils/pm/kiss_thin_nfa_analyze.cc | 1499 + components/utils/pm/kiss_thin_nfa_base.h | 261 + components/utils/pm/kiss_thin_nfa_build.cc | 242 + components/utils/pm/kiss_thin_nfa_compile.cc | 2232 ++ components/utils/pm/kiss_thin_nfa_impl.h | 189 + components/utils/pm/lss_example.txt | 13760 +++++++ components/utils/pm/pm_adaptor.cc | 103 + components/utils/pm/pm_adaptor.h | 229 + components/utils/pm/pm_hook.cc | 165 + components/utils/pm/pm_ut/CMakeLists.txt | 5 + components/utils/pm/pm_ut/pm_pat_ut.cc | 78 + components/utils/pm/pm_ut/pm_scan_ut.cc | 469 + core/CMakeLists.txt | 48 + core/README.md | 23 + core/agent_core_utilities/CMakeLists.txt | 3 + .../agent_core_utilities.cc | 346 + .../agent_core_utilities_ut/CMakeLists.txt | 7 + .../agent_core_utilities_ut.cc | 98 + core/agent_details/CMakeLists.txt | 3 + core/agent_details/agent_details.cc | 307 + .../agent_details_ut/CMakeLists.txt | 9 + .../agent_details_ut/agent_details_ut.cc | 156 + core/agent_details_reporter/CMakeLists.txt | 3 + .../agent_details_report.cc | 60 + .../agent_details_reporter.cc | 399 + .../agent_details_reporter_ut/CMakeLists.txt | 7 + .../agent_details_reporter_ut.cc | 456 + core/attachments/CMakeLists.txt | 1 + .../http_configuration/CMakeLists.txt | 3 + .../http_configuration/http_configuration.cc | 204 + .../http_configuration_ut/CMakeLists.txt | 1 + .../http_configuration_ut.cc | 111 + core/buffers/CMakeLists.txt | 3 + core/buffers/buffer.cc | 530 + core/buffers/buffer_eval.cc | 43 + core/buffers/buffers_ut/CMakeLists.txt | 7 + core/buffers/buffers_ut/buffer_eval_ut.cc | 54 + core/buffers/buffers_ut/buffers_ut.cc | 733 + core/buffers/char_iterator.cc | 93 + core/buffers/data_container.cc | 35 + core/buffers/segment.cc | 162 + core/compression/CMakeLists.txt | 9 + core/compression/compression_utils.cc | 384 + .../compression_utils_ut/CMakeLists.txt | 5 + .../compression_utils_ut.cc | 459 + .../test_files/chunk_sized_compressed_file.gz | Bin 0 -> 51247 bytes .../test_files/chunk_sized_compressed_file.zz | Bin 0 -> 51226 bytes .../test_files/chunk_sized_string | Bin 0 -> 51200 bytes .../multiple_chunk_sized_compressed_file.gz | Bin 0 -> 204881 bytes .../multiple_chunk_sized_compressed_file.zz | Bin 0 -> 204871 bytes .../test_files/multiple_chunk_sized_string | Bin 0 -> 204800 bytes ...ut_of_output_space_test_compressed_file.gz | Bin 0 -> 274 bytes core/config/CMakeLists.txt | 4 + core/config/config.cc | 850 + core/config/config_globals.cc | 89 + core/config/config_specific.cc | 79 + core/config/include/profile_settings.h | 63 + core/connkey/CMakeLists.txt | 1 + core/connkey/connkey.cc | 241 + core/connkey/connkey_eval.cc | 167 + core/core_ut/CMakeLists.txt | 5 + core/core_ut/cache_ut.cc | 172 + core/core_ut/common_ut.cc | 233 + core/core_ut/enum_array_ut.cc | 85 + core/core_ut/enum_range_ut.cc | 50 + core/core_ut/maybe_res_ut.cc | 406 + core/core_ut/tostring_ut.cc | 42 + core/core_ut/virtual_container_ut.cc | 116 + core/cptest/CMakeLists.txt | 10 + core/cptest/cptest.cc | 145 + core/cptest/cptest_data_buf.cc | 48 + core/cptest/cptest_tcppacket.cc | 593 + core/cptest/cptest_ut/CMakeLists.txt | 5 + core/cptest/cptest_ut/cptest_packet_ut.cc | 222 + core/cptest/cptest_ut/cptest_ut.cc | 67 + core/cpu/CMakeLists.txt | 3 + core/cpu/cpu.cc | 330 + core/cpu/cpu_ut/CMakeLists.txt | 5 + core/cpu/cpu_ut/cpu_ut.cc | 643 + core/debug_is/CMakeLists.txt | 3 + core/debug_is/debug.cc | 746 + core/debug_is/debug_ex.h | 137 + core/debug_is/debug_is_ut/CMakeLists.txt | 6 + core/debug_is/debug_is_ut/debug_ut.cc | 1060 + core/debug_is/debug_streams.cc | 389 + core/encryptor/CMakeLists.txt | 7 + core/encryptor/cpnano_base64/CMakeLists.txt | 6 + core/encryptor/cpnano_base64/base64.cc | 68 + core/encryptor/cpnano_base64/base64.h | 29 + core/encryptor/cpnano_base64/cpnano_base64.cc | 52 + core/encryptor/encryptor.cc | 78 + core/encryptor/encryptor_ut/CMakeLists.txt | 7 + core/encryptor/encryptor_ut/encryptor_ut.cc | 85 + core/environment/CMakeLists.txt | 3 + core/environment/base_evaluators.cc | 103 + core/environment/context.cc | 101 + core/environment/environment.cc | 421 + .../environment/environment_ut/CMakeLists.txt | 5 + .../environment_ut/base_evaluators_ut.cc | 199 + core/environment/environment_ut/context_ut.cc | 237 + .../environment_ut/environment_rest_ut.cc | 68 + .../environment_ut/environment_ut.cc | 87 + core/environment/environment_ut/parsing_ut.cc | 156 + core/environment/environment_ut/span_ut.cc | 95 + core/environment/environment_ut/trace_ut.cc | 59 + core/environment/environment_ut/tracing_ut.cc | 384 + core/environment/evaluator_registration.h | 19 + core/environment/param_attr.cc | 23 + core/environment/parsing.cc | 137 + core/environment/span.cc | 131 + core/environment/trace.cc | 58 + core/event_is/CMakeLists.txt | 3 + core/event_is/event_ut/CMakeLists.txt | 1 + core/event_is/event_ut/event_ut.cc | 242 + core/event_is/listener.cc | 41 + core/include/attachments/attachment_types.h | 31 + core/include/attachments/compression_utils.h | 86 + core/include/attachments/http_configuration.h | 68 + .../attachments/nginx_attachment_common.h | 279 + .../attachments/nginx_attachment_util.h | 67 + core/include/attachments/shmem_ipc.h | 66 + core/include/general/buffer.h | 200 + core/include/general/buffer/char_iterator.h | 53 + core/include/general/buffer/data_container.h | 71 + .../include/general/buffer/helper_functions.h | 46 + core/include/general/buffer/internal_ptr.h | 53 + core/include/general/buffer/segment.h | 75 + core/include/general/c_common/ip_common.h | 55 + core/include/general/c_common/network_defs.h | 265 + .../general/c_common/networking_headers.h | 211 + core/include/general/common.h | 138 + core/include/general/config_component.h | 56 + core/include/general/cptest.h | 43 + core/include/general/cptest/cptest_basic.h | 36 + core/include/general/cptest/cptest_file.h | 37 + core/include/general/cptest/cptest_maybe.h | 164 + .../include/general/cptest/cptest_singleton.h | 27 + .../include/general/cptest/cptest_tcppacket.h | 88 + core/include/general/debug.h | 269 + core/include/general/encryptor.h | 36 + core/include/general/environment.h | 45 + core/include/general/hash_combine.h | 25 + core/include/general/impl/singleton.h | 162 + core/include/general/intelligence_comp_v2.h | 46 + core/include/general/logging_comp.h | 55 + core/include/general/mainloop.h | 54 + core/include/general/maybe_res.h | 467 + core/include/general/rest_server.h | 46 + core/include/general/scope_exit.h | 91 + core/include/general/shmpktqueue.h | 71 + core/include/general/singleton.h | 75 + core/include/general/table.h | 51 + core/include/general/table/entry_impl.h | 220 + core/include/general/table/expiration_impl.h | 117 + core/include/general/table/table_helpers.h | 55 + core/include/general/table/table_impl.h | 442 + core/include/general/table/table_list.h | 96 + core/include/general/table/table_list_iter.h | 98 + core/include/general/table/table_list_node.h | 87 + core/include/general/tenant_manager.h | 52 + core/include/general/time_proxy.h | 34 + core/include/general/tostring.h | 72 + .../include/internal/agent_details_reporter.h | 49 + core/include/internal/http_encoder.h | 95 + core/include/internal/instance_awareness.h | 35 + core/include/internal/ioctl_is.h | 41 + .../internal/mainloop/mainloop_metric.h | 96 + core/include/internal/messaging_buffer.h | 52 + .../messaging_buffer/bucket_manager.h | 58 + .../internal/messaging_buffer/event_queue.h | 125 + .../messaging_buffer/http_request_event.h | 120 + core/include/internal/proto_message_comp.h | 57 + core/include/internal/shell_cmd.h | 43 + core/include/internal/trap_handler.h | 45 + .../services_sdk/interfaces/i_agent_details.h | 62 + .../interfaces/i_agent_details_reporter.h | 55 + core/include/services_sdk/interfaces/i_cpu.h | 41 + .../services_sdk/interfaces/i_encryptor.h | 44 + .../services_sdk/interfaces/i_environment.h | 116 + .../services_sdk/interfaces/i_failopen.h | 22 + .../interfaces/i_health_check_manager.h | 31 + .../interfaces/i_instance_awareness.h | 33 + .../interfaces/i_intelligence_is_v2.h | 285 + .../include/services_sdk/interfaces/i_ioctl.h | 29 + .../services_sdk/interfaces/i_logging.h | 41 + .../services_sdk/interfaces/i_mainloop.h | 87 + .../services_sdk/interfaces/i_messaging.h | 194 + .../interfaces/i_messaging_buffer.h | 33 + .../interfaces/i_messaging_downloader.h | 38 + .../services_sdk/interfaces/i_rest_api.h | 56 + .../services_sdk/interfaces/i_shell_cmd.h | 34 + .../interfaces/i_signal_handler.h | 27 + .../services_sdk/interfaces/i_socket_is.h | 46 + .../include/services_sdk/interfaces/i_table.h | 86 + .../services_sdk/interfaces/i_table_iter.h | 28 + .../interfaces/i_tenant_manager.h | 49 + .../services_sdk/interfaces/i_time_get.h | 33 + .../services_sdk/interfaces/i_time_set.h | 29 + .../services_sdk/interfaces/i_trap_handler.h | 33 + .../intelligence_is_v2/asset_source_v2.h | 71 + .../intelligence_is_v2/asset_source_v2_impl.h | 45 + .../intelligence_query_v2.h | 55 + .../intelligence_query_v2_impl.h | 100 + .../intelligence_types_v2.h | 80 + .../intelligence_is_v2/query_filter_v2.h | 90 + .../intelligence_is_v2/query_request_v2.h | 92 + .../intelligence_is_v2/query_response_v2.h | 120 + .../query_response_v2_impl.h | 138 + .../intelligence_is_v2/query_types_v2.h | 53 + .../requested_attributes_v2.h | 60 + .../interfaces/messaging/http_core.h | 103 + .../interfaces/mock/mock_agent_details.h | 42 + .../mock/mock_agent_details_reporter.h | 32 + .../services_sdk/interfaces/mock/mock_cpu.h | 15 + .../interfaces/mock/mock_encryptor.h | 26 + .../interfaces/mock/mock_environment.h | 49 + .../interfaces/mock/mock_instance_awareness.h | 20 + .../interfaces/mock/mock_logging.h | 19 + .../interfaces/mock/mock_mainloop.h | 41 + .../interfaces/mock/mock_messaging.h | 76 + .../interfaces/mock/mock_rest_api.h | 22 + .../interfaces/mock/mock_shell_cmd.h | 24 + .../interfaces/mock/mock_socket_is.h | 19 + .../services_sdk/interfaces/mock/mock_table.h | 31 + .../interfaces/mock/mock_tenant_manager.h | 34 + .../interfaces/mock/mock_time_get.h | 14 + .../services_sdk/resources/agent_details.h | 89 + .../resources/agent_details_report.h | 51 + .../services_sdk/resources/component.h | 39 + .../component_is/components_list_impl.h | 236 + .../component_is/node_components_impl.h | 54 + .../services_sdk/resources/components_list.h | 35 + core/include/services_sdk/resources/config.h | 98 + .../resources/config/config_exception.h | 36 + .../resources/config/config_impl.h | 256 + .../resources/config/config_loader.h | 86 + .../resources/config/config_types.h | 33 + .../resources/config/generic_config.h | 134 + .../services_sdk/resources/config/i_config.h | 106 + .../resources/config/i_config_iterator.h | 35 + .../resources/config/range_config.h | 85 + .../resources/config/type_wrapper.h | 79 + core/include/services_sdk/resources/context.h | 114 + core/include/services_sdk/resources/cpu.h | 106 + .../services_sdk/resources/cpu/cpu_metric.h | 65 + .../resources/cpu/failopen_mode_status.h | 44 + .../services_sdk/resources/debug_flags.h | 159 + .../resources/environment/base_evaluators.h | 82 + .../resources/environment/context_impl.h | 175 + .../environment/evaluator_templates.h | 102 + .../resources/environment/evaluators_repo.h | 107 + .../resources/environment/param.h | 50 + .../resources/environment/parsing_functions.h | 77 + .../services_sdk/resources/environment/span.h | 66 + .../resources/environment/trace.h | 43 + .../resources/environment/tracing_metric.h | 83 + .../resources/environment_evaluator.h | 76 + core/include/services_sdk/resources/event.h | 24 + .../resources/event_is/event_impl.h | 51 + .../resources/event_is/listener_impl.h | 99 + .../services_sdk/resources/generic_metric.h | 116 + .../health_check_status/health_check_status.h | 99 + .../resources/intelligence_filter.h | 58 + .../resources/intelligence_is/data_map.h | 63 + .../resources/intelligence_is/data_string.h | 36 + .../resources/intelligence_is/data_vector.h | 48 + .../intelligence_is/read_attribute_impl.h | 55 + .../intelligence_is_v2/data_map_v2.h | 103 + .../intelligence_is_v2/data_string_v2.h | 36 + .../intelligence_is_v2/data_vector_v2.h | 54 + .../read_attribute_v2_impl.h | 55 + .../include/services_sdk/resources/listener.h | 24 + .../services_sdk/resources/log_generator.h | 110 + .../resources/memory_consumption.h | 42 + .../resources/metric/all_metric_event.h | 40 + .../services_sdk/resources/metric/average.h | 70 + .../services_sdk/resources/metric/counter.h | 67 + .../resources/metric/last_reported_value.h | 68 + .../services_sdk/resources/metric/max.h | 72 + .../resources/metric/metric_calc.h | 42 + .../resources/metric/metric_map.h | 88 + .../services_sdk/resources/metric/min.h | 69 + .../resources/metric/top_values.h | 84 + .../services_sdk/resources/read_attribute.h | 37 + .../resources/read_attribute_v2.h | 37 + .../resources/report/base_field.h | 279 + .../services_sdk/resources/report/log_rest.h | 115 + .../services_sdk/resources/report/report.h | 406 + .../resources/report/report_bulks.h | 84 + .../resources/report/report_enums.h | 144 + core/include/services_sdk/resources/rest.h | 336 + .../resources/table/i_table_impl.h | 55 + .../resources/table/opaque_basic.h | 47 + .../services_sdk/resources/table/opaque_reg.h | 62 + .../resources/table/opaque_repo.h | 49 + .../services_sdk/resources/table_iter.h | 39 + .../services_sdk/resources/table_opaque.h | 45 + .../resources/tag_and_enum_management.h | 51 + core/include/services_sdk/resources/version.h | 44 + .../utilities/agent_core_utilities.h | 78 + core/include/services_sdk/utilities/cache.h | 87 + .../utilities/caching/cache_impl.h | 245 + .../utilities/caching/cache_types.h | 83 + core/include/services_sdk/utilities/connkey.h | 332 + .../utilities/customized_cereal_map.h | 64 + .../utilities/customized_cereal_multimap.h | 78 + .../services_sdk/utilities/enum_array.h | 57 + .../services_sdk/utilities/enum_range.h | 156 + core/include/services_sdk/utilities/flags.h | 47 + .../services_sdk/utilities/fog_rest_error.h | 30 + .../services_sdk/utilities/rest/rest_helper.h | 25 + .../services_sdk/utilities/rest/rest_param.h | 65 + .../utilities/rest/schema_printer.h | 138 + core/include/services_sdk/utilities/sasal.h | 28 + .../services_sdk/utilities/socket_is.h | 39 + .../services_sdk/utilities/time_print.h | 50 + .../utilities/virtual_container.h | 262 + .../utilities/virtual_modifiers.h | 157 + core/instance_awareness/CMakeLists.txt | 3 + core/instance_awareness/instance_awareness.cc | 93 + .../instance_awareness_ut/CMakeLists.txt | 5 + .../instance_awareness_ut.cc | 130 + core/intelligence_is_v2/CMakeLists.txt | 7 + .../intelligence_comp_v2.cc | 145 + .../intelligence_is_v2_ut/CMakeLists.txt | 7 + .../intelligence_comp_v2_ut.cc | 678 + .../offline_intelligence_files_v2/1.2.3.4 | 49 + .../query_request_v2_ut.cc | 440 + .../query_response_v2_ut.cc | 464 + .../intelligence_types_v2.cc | 91 + core/intelligence_is_v2/query_filter_v2.cc | 141 + core/intelligence_is_v2/query_request_v2.cc | 173 + core/intelligence_is_v2/query_types_v2.cc | 38 + .../requested_attributes_v2.cc | 63 + core/logging/CMakeLists.txt | 3 + core/logging/cef_stream.cc | 81 + core/logging/debug_stream.cc | 34 + core/logging/file_stream.cc | 139 + core/logging/fog_stream.cc | 64 + core/logging/log_generator.cc | 110 + core/logging/log_streams.h | 119 + core/logging/logging.cc | 339 + core/logging/logging_metric.h | 73 + core/logging/logging_ut/CMakeLists.txt | 7 + core/logging/logging_ut/logging_ut.cc | 1333 + core/logging/syslog_stream.cc | 92 + core/mainloop/CMakeLists.txt | 6 + core/mainloop/coroutine.cc | 84 + core/mainloop/coroutine.h | 63 + core/mainloop/mainloop.cc | 592 + core/mainloop/mainloop_ut/CMakeLists.txt | 11 + core/mainloop/mainloop_ut/mainloop_ut.cc | 531 + core/memory_consumption/CMakeLists.txt | 3 + core/memory_consumption/memory_consumption.cc | 163 + .../memory_consumption_ut/CMakeLists.txt | 7 + .../memory_consumption_ut.cc | 76 + core/memory_consumption/memory_metric.h | 120 + core/message/CMakeLists.txt | 4 + core/message/http_core.cc | 113 + core/message/http_decoder.cc | 358 + core/message/http_decoder.h | 55 + core/message/http_encoder.cc | 176 + core/message/i_message_decoder.h | 26 + core/message/message.cc | 2081 + core/message/message_metric.h | 58 + core/message/smart_bio.h | 74 + core/messaging_buffer/CMakeLists.txt | 6 + core/messaging_buffer/bucket_manager.cc | 217 + core/messaging_buffer/event_queue.cc | 846 + core/messaging_buffer/messaging_buffer.cc | 275 + .../messaging_buffer_ut/CMakeLists.txt | 6 + .../messaging_buffer_ut.cc | 755 + core/metric/CMakeLists.txt | 3 + core/metric/generic_metric.cc | 243 + core/metric/metric_ut/CMakeLists.txt | 5 + core/metric/metric_ut/metric_ut.cc | 1071 + core/report/CMakeLists.txt | 3 + core/report/report.cc | 296 + core/report/report_ut/CMakeLists.txt | 5 + core/report/report_ut/report_ut.cc | 698 + core/report/tag_and_enum_management.cc | 318 + core/rest/CMakeLists.txt | 4 + core/rest/i_rest_invoke.h | 32 + core/rest/rest.cc | 129 + core/rest/rest_conn.cc | 158 + core/rest/rest_conn.h | 40 + core/rest/rest_server.cc | 261 + core/rest/rest_ut/CMakeLists.txt | 8 + core/rest/rest_ut/rest_config_ut.cc | 108 + core/rest/rest_ut/rest_must_param_ut.cc | 81 + core/rest/rest_ut/rest_schema_ut.cc | 524 + core/shell_cmd/CMakeLists.txt | 1 + core/shell_cmd/shell_cmd.cc | 136 + core/shm_pkt_queue/CMakeLists.txt | 13 + core/shm_pkt_queue/shm_pkt_queue.cc | 210 + .../shm_pkt_queue_ut/CMakeLists.txt | 9 + .../shm_pkt_queue_ut/shm_pkt_queue_ut.cc | 291 + core/shmem_ipc/CMakeLists.txt | 10 + core/shmem_ipc/shared_ipc_debug.h | 31 + core/shmem_ipc/shared_ring_queue.c | 556 + core/shmem_ipc/shared_ring_queue.h | 75 + core/shmem_ipc/shmem_ipc.c | 289 + core/shmem_ipc/shmem_ipc_ut/CMakeLists.txt | 2 + .../shmem_ipc_ut/shared_ring_queue_ut.cc | 339 + core/shmem_ipc/shmem_ipc_ut/shmem_ipc_ut.cc | 271 + core/singleton/CMakeLists.txt | 3 + core/singleton/singleton.cc | 44 + core/singleton/singleton_ut/CMakeLists.txt | 5 + core/singleton/singleton_ut/singleton_ut.cc | 209 + core/socket_is/CMakeLists.txt | 1 + core/socket_is/socket_is.cc | 649 + core/table/CMakeLists.txt | 1 + core/table/opaque_repo.cc | 30 + core/table/table.cc | 16 + core/tenant_manager/CMakeLists.txt | 1 + core/tenant_manager/tenant_manager.cc | 328 + core/time_proxy/CMakeLists.txt | 3 + core/time_proxy/time_proxy.cc | 137 + core/time_proxy/time_proxy_ut/CMakeLists.txt | 1 + .../time_proxy/time_proxy_ut/time_print_ut.cc | 45 + .../time_proxy/time_proxy_ut/time_proxy_ut.cc | 67 + core/version/CMakeLists.txt | 13 + core/version/build_version_vars_h.py | 52 + core/version/version.cc | 125 + core/version/version_ut/CMakeLists.txt | 5 + core/version/version_ut/version_ut.cc | 101 + cppcheck.cmake | 137 + external/C-Mock/.travis.yml | 13 + external/C-Mock/LICENSE.md | 9 + external/C-Mock/README.md | 269 + external/C-Mock/bin/cmock-config | 76 + .../cmock/cmock-function-class-mockers.h | 370 + .../include/cmock/cmock-function-mockers.h | 1082 + .../C-Mock/include/cmock/cmock-internal.h | 43 + .../include/cmock/cmock-spec-builders.h | 45 + external/C-Mock/include/cmock/cmock.h | 42 + ...k-function-class-mockers-old-style_test.cc | 49 + .../test/cmock-function-class-mockers_test.cc | 60 + .../test/cmock-function-mockers_test.cc | 50 + .../C-Mock/test/cmock-spec-builders_test.cc | 17 + external/C-Mock/test/math.c | 11 + external/C-Mock/test/math.h | 18 + external/CMakeLists.txt | 1 + external/cereal/LICENSE | 24 + external/cereal/access.hpp | 448 + external/cereal/archives/adapters.hpp | 163 + external/cereal/archives/binary.hpp | 169 + external/cereal/archives/json.hpp | 976 + external/cereal/archives/portable_binary.hpp | 334 + external/cereal/archives/xml.hpp | 897 + external/cereal/cereal.hpp | 1063 + external/cereal/details/helpers.hpp | 381 + external/cereal/details/polymorphic_impl.hpp | 688 + .../cereal/details/polymorphic_impl_fwd.hpp | 65 + external/cereal/details/static_object.hpp | 132 + external/cereal/details/traits.hpp | 1389 + external/cereal/details/util.hpp | 84 + external/cereal/external/base64.hpp | 129 + .../cereal/external/rapidjson/allocators.h | 693 + .../external/rapidjson/cursorstreamwrapper.h | 78 + external/cereal/external/rapidjson/document.h | 3043 ++ .../cereal/external/rapidjson/encodedstream.h | 299 + .../cereal/external/rapidjson/encodings.h | 716 + external/cereal/external/rapidjson/error/en.h | 122 + .../cereal/external/rapidjson/error/error.h | 216 + .../external/rapidjson/filereadstream.h | 99 + .../external/rapidjson/filewritestream.h | 104 + external/cereal/external/rapidjson/fwd.h | 151 + .../external/rapidjson/internal/biginteger.h | 297 + .../external/rapidjson/internal/clzll.h | 71 + .../external/rapidjson/internal/diyfp.h | 261 + .../cereal/external/rapidjson/internal/dtoa.h | 249 + .../external/rapidjson/internal/ieee754.h | 78 + .../cereal/external/rapidjson/internal/itoa.h | 308 + .../cereal/external/rapidjson/internal/meta.h | 186 + .../external/rapidjson/internal/pow10.h | 55 + .../external/rapidjson/internal/regex.h | 739 + .../external/rapidjson/internal/stack.h | 232 + .../external/rapidjson/internal/strfunc.h | 83 + .../external/rapidjson/internal/strtod.h | 293 + .../cereal/external/rapidjson/internal/swap.h | 46 + .../external/rapidjson/istreamwrapper.h | 128 + .../cereal/external/rapidjson/memorybuffer.h | 70 + .../cereal/external/rapidjson/memorystream.h | 71 + .../external/rapidjson/msinttypes/inttypes.h | 316 + .../external/rapidjson/msinttypes/stdint.h | 300 + .../external/rapidjson/ostreamwrapper.h | 81 + external/cereal/external/rapidjson/pointer.h | 1482 + .../cereal/external/rapidjson/prettywriter.h | 277 + .../cereal/external/rapidjson/rapidjson.h | 741 + external/cereal/external/rapidjson/reader.h | 2246 ++ external/cereal/external/rapidjson/schema.h | 2816 ++ external/cereal/external/rapidjson/stream.h | 223 + .../cereal/external/rapidjson/stringbuffer.h | 121 + external/cereal/external/rapidjson/uri.h | 481 + external/cereal/external/rapidjson/writer.h | 710 + external/cereal/external/rapidxml/license.txt | 52 + external/cereal/external/rapidxml/manual.html | 406 + .../cereal/external/rapidxml/rapidxml.hpp | 2624 ++ .../external/rapidxml/rapidxml_iterators.hpp | 175 + .../external/rapidxml/rapidxml_print.hpp | 426 + .../external/rapidxml/rapidxml_utils.hpp | 123 + external/cereal/macros.hpp | 121 + external/cereal/types/array.hpp | 79 + external/cereal/types/base_class.hpp | 201 + external/cereal/types/bitset.hpp | 174 + external/cereal/types/boost_variant.hpp | 106 + external/cereal/types/chrono.hpp | 72 + external/cereal/types/common.hpp | 129 + external/cereal/types/complex.hpp | 56 + .../concepts/pair_associative_container.hpp | 73 + external/cereal/types/deque.hpp | 62 + external/cereal/types/forward_list.hpp | 68 + external/cereal/types/functional.hpp | 43 + external/cereal/types/list.hpp | 62 + external/cereal/types/map.hpp | 36 + external/cereal/types/memory.hpp | 485 + external/cereal/types/polymorphic.hpp | 481 + external/cereal/types/queue.hpp | 132 + external/cereal/types/set.hpp | 103 + external/cereal/types/stack.hpp | 76 + external/cereal/types/string.hpp | 61 + external/cereal/types/tuple.hpp | 123 + external/cereal/types/unordered_map.hpp | 36 + external/cereal/types/unordered_set.hpp | 99 + external/cereal/types/utility.hpp | 47 + external/cereal/types/valarray.hpp | 89 + external/cereal/types/vector.hpp | 112 + external/makeself/COPYING | 339 + external/makeself/README.md | 203 + external/makeself/makeself-header.sh | 600 + external/makeself/makeself.1 | 104 + external/makeself/makeself.lsm | 16 + external/makeself/makeself.sh | 618 + external/makeself/test/datetest | 131 + external/picojson/.clang-format | 4 + external/picojson/.travis.yml | 8 + external/picojson/Changes | 25 + external/picojson/LICENSE | 25 + external/picojson/README.mkdn | 195 + external/picojson/examples/github-issues.cc | 106 + external/picojson/examples/iostream.cc | 69 + external/picojson/examples/streaming.cc | 72 + external/picojson/picojson.h | 1208 + external/picojson/picotest/picotest.c | 99 + external/picojson/picotest/picotest.h | 39 + external/picojson/test.cc | 375 + external/yajl/BUILDING | 23 + external/yajl/BUILDING.win32 | 27 + external/yajl/CMakeLists.txt | 81 + external/yajl/COPYING | 13 + external/yajl/ChangeLog | 189 + external/yajl/README | 74 + external/yajl/TODO | 9 + external/yajl/YAJLDoc.cmake | 26 + external/yajl/configure | 81 + external/yajl/example/CMakeLists.txt | 23 + external/yajl/example/README.md | 7 + external/yajl/example/parse_config | Bin 0 -> 36928 bytes external/yajl/example/parse_config.c | 69 + external/yajl/example/sample.config | 101 + external/yajl/perf/CMakeLists.txt | 23 + external/yajl/perf/documents.c | 1418 + external/yajl/perf/documents.h | 28 + external/yajl/perf/perftest | Bin 0 -> 122416 bytes external/yajl/perf/perftest.c | 134 + external/yajl/reformatter/CMakeLists.txt | 43 + external/yajl/reformatter/json_reformat.c | 210 + external/yajl/src/CMakeLists.txt | 88 + external/yajl/src/YAJL.dxy | 1258 + external/yajl/src/api/yajl_common.h | 75 + external/yajl/src/api/yajl_gen.h | 167 + external/yajl/src/api/yajl_parse.h | 226 + external/yajl/src/api/yajl_tree.h | 186 + external/yajl/src/api/yajl_version.h.cmake | 23 + external/yajl/src/yajl | 33 + external/yajl/src/yajl.c | 175 + external/yajl/src/yajl.pc.cmake | 9 + external/yajl/src/yajl_alloc.c | 52 + external/yajl/src/yajl_alloc.h | 34 + external/yajl/src/yajl_buf.c | 103 + external/yajl/src/yajl_buf.h | 57 + external/yajl/src/yajl_bytestack.h | 69 + external/yajl/src/yajl_encode.c | 220 + external/yajl/src/yajl_encode.h | 34 + external/yajl/src/yajl_gen.c | 362 + external/yajl/src/yajl_lex.c | 763 + external/yajl/src/yajl_lex.h | 117 + external/yajl/src/yajl_parser.c | 498 + external/yajl/src/yajl_parser.h | 78 + external/yajl/src/yajl_tree.c | 503 + external/yajl/src/yajl_version.c | 7 + external/yajl/test/CMakeLists.txt | 16 + external/yajl/test/api/CMakeLists.txt | 25 + external/yajl/test/api/gen-extra-close | Bin 0 -> 8712 bytes external/yajl/test/api/gen-extra-close.c | 19 + external/yajl/test/api/run_tests.sh | 23 + external/yajl/test/parsing/CMakeLists.txt | 23 + ...ficult_json_c_test_case_with_comments.json | 1 + ...t_json_c_test_case_with_comments.json.gold | 36 + .../cases/ac_simple_with_comments.json | 11 + .../cases/ac_simple_with_comments.json.gold | 9 + .../parsing/cases/ag_false_then_garbage.json | 1 + .../cases/ag_false_then_garbage.json.gold | 2 + .../parsing/cases/ag_null_then_garbage.json | 1 + .../cases/ag_null_then_garbage.json.gold | 2 + .../parsing/cases/ag_true_then_garbage.json | 1 + .../cases/ag_true_then_garbage.json.gold | 2 + external/yajl/test/parsing/cases/am_eof.json | 1 + .../yajl/test/parsing/cases/am_eof.json.gold | 4 + .../yajl/test/parsing/cases/am_integers.json | 1 + .../test/parsing/cases/am_integers.json.gold | 3 + .../yajl/test/parsing/cases/am_multiple.json | 3 + .../test/parsing/cases/am_multiple.json.gold | 5 + .../yajl/test/parsing/cases/am_stuff.json | 7 + .../test/parsing/cases/am_stuff.json.gold | 14 + .../test/parsing/cases/ap_array_open.json | 1 + .../parsing/cases/ap_array_open.json.gold | 2 + .../yajl/test/parsing/cases/ap_eof_str.json | 1 + .../test/parsing/cases/ap_eof_str.json.gold | 1 + .../yajl/test/parsing/cases/ap_map_open.json | 1 + .../test/parsing/cases/ap_map_open.json.gold | 2 + .../test/parsing/cases/ap_partial_ok.json | 1 + .../parsing/cases/ap_partial_ok.json.gold | 4 + external/yajl/test/parsing/cases/array.json | 6 + .../yajl/test/parsing/cases/array.json.gold | 22 + .../yajl/test/parsing/cases/array_close.json | 1 + .../test/parsing/cases/array_close.json.gold | 2 + external/yajl/test/parsing/cases/bignums.json | 1 + .../yajl/test/parsing/cases/bignums.json.gold | 5 + .../yajl/test/parsing/cases/bogus_char.json | 4 + .../test/parsing/cases/bogus_char.json.gold | 10 + .../cases/codepoints_from_unicode_org.json | 1 + .../codepoints_from_unicode_org.json.gold | 2 + .../yajl/test/parsing/cases/deep_arrays.json | 1 + .../test/parsing/cases/deep_arrays.json.gold | 2049 + .../cases/difficult_json_c_test_case.json | 1 + .../difficult_json_c_test_case.json.gold | 36 + external/yajl/test/parsing/cases/doubles.json | 1 + .../yajl/test/parsing/cases/doubles.json.gold | 7 + .../test/parsing/cases/doubles_in_array.json | 1 + .../parsing/cases/doubles_in_array.json.gold | 8 + .../yajl/test/parsing/cases/empty_array.json | 1 + .../test/parsing/cases/empty_array.json.gold | 3 + .../yajl/test/parsing/cases/empty_string.json | 1 + .../test/parsing/cases/empty_string.json.gold | 2 + .../test/parsing/cases/escaped_bulgarian.json | 4 + .../parsing/cases/escaped_bulgarian.json.gold | 7 + .../test/parsing/cases/escaped_foobar.json | 1 + .../parsing/cases/escaped_foobar.json.gold | 2 + external/yajl/test/parsing/cases/false.json | 1 + .../yajl/test/parsing/cases/false.json.gold | 2 + .../parsing/cases/fg_false_then_garbage.json | 1 + .../cases/fg_false_then_garbage.json.gold | 3 + .../yajl/test/parsing/cases/fg_issue_7.json | 1 + .../test/parsing/cases/fg_issue_7.json.gold | 3 + .../parsing/cases/fg_null_then_garbage.json | 1 + .../cases/fg_null_then_garbage.json.gold | 3 + .../parsing/cases/fg_true_then_garbage.json | 1 + .../cases/fg_true_then_garbage.json.gold | 3 + .../test/parsing/cases/four_byte_utf8.json | 2 + .../parsing/cases/four_byte_utf8.json.gold | 5 + .../test/parsing/cases/high_overflow.json | 1 + .../parsing/cases/high_overflow.json.gold | 2 + .../yajl/test/parsing/cases/integers.json | 3 + .../test/parsing/cases/integers.json.gold | 14 + .../yajl/test/parsing/cases/invalid_utf8.json | 1 + .../test/parsing/cases/invalid_utf8.json.gold | 3 + .../cases/isolated_surrogate_marker.json | 1 + .../cases/isolated_surrogate_marker.json.gold | 2 + .../parsing/cases/leading_zero_in_number.json | 1 + .../cases/leading_zero_in_number.json.gold | 5 + .../test/parsing/cases/lonely_minus_sign.json | 7 + .../parsing/cases/lonely_minus_sign.json.gold | 9 + .../test/parsing/cases/lonely_number.json | 1 + .../parsing/cases/lonely_number.json.gold | 2 + .../yajl/test/parsing/cases/low_overflow.json | 1 + .../test/parsing/cases/low_overflow.json.gold | 2 + .../yajl/test/parsing/cases/map_close.json | 1 + .../test/parsing/cases/map_close.json.gold | 2 + .../missing_integer_after_decimal_point.json | 1 + ...sing_integer_after_decimal_point.json.gold | 2 + .../cases/missing_integer_after_exponent.json | 1 + .../missing_integer_after_exponent.json.gold | 2 + .../yajl/test/parsing/cases/multiple.json | 3 + .../test/parsing/cases/multiple.json.gold | 4 + .../cases/non_utf8_char_in_string.json | 1 + .../cases/non_utf8_char_in_string.json.gold | 8 + .../test/parsing/cases/np_partial_bad.json | 1 + .../parsing/cases/np_partial_bad.json.gold | 5 + external/yajl/test/parsing/cases/null.json | 1 + .../yajl/test/parsing/cases/null.json.gold | 2 + .../test/parsing/cases/nulls_and_bools.json | 5 + .../parsing/cases/nulls_and_bools.json.gold | 9 + external/yajl/test/parsing/cases/simple.json | 5 + .../yajl/test/parsing/cases/simple.json.gold | 9 + .../parsing/cases/simple_with_comments.json | 11 + .../cases/simple_with_comments.json.gold | 5 + .../parsing/cases/string_invalid_escape.json | 1 + .../cases/string_invalid_escape.json.gold | 3 + .../cases/string_invalid_hex_char.json | 1 + .../cases/string_invalid_hex_char.json.gold | 2 + .../parsing/cases/string_with_escapes.json | 3 + .../cases/string_with_escapes.json.gold | 7 + .../cases/string_with_invalid_newline.json | 2 + .../string_with_invalid_newline.json.gold | 2 + .../test/parsing/cases/three_byte_utf8.json | 1 + .../parsing/cases/three_byte_utf8.json.gold | 7 + external/yajl/test/parsing/cases/true.json | 1 + .../yajl/test/parsing/cases/true.json.gold | 2 + .../parsing/cases/unescaped_bulgarian.json | 1 + .../cases/unescaped_bulgarian.json.gold | 4 + .../yajl/test/parsing/cases/zerobyte.json | 1 + .../test/parsing/cases/zerobyte.json.gold | Bin 0 -> 28 bytes external/yajl/test/parsing/run_tests.sh | 94 + external/yajl/test/parsing/yajl_test | Bin 0 -> 36928 bytes external/yajl/test/parsing/yajl_test.c | 291 + external/yajl/verify/CMakeLists.txt | 37 + external/yajl/verify/json_verify | Bin 0 -> 31832 bytes external/yajl/verify/json_verify.c | 120 + external/yajl/yajl-2.1.1/bin/json_reformat | Bin 0 -> 41424 bytes external/yajl/yajl-2.1.1/bin/json_verify | Bin 0 -> 31832 bytes .../yajl-2.1.1/include/yajl/yajl_common.h | 75 + .../yajl/yajl-2.1.1/include/yajl/yajl_gen.h | 167 + .../yajl/yajl-2.1.1/include/yajl/yajl_parse.h | 226 + .../yajl/yajl-2.1.1/include/yajl/yajl_tree.h | 186 + .../yajl-2.1.1/include/yajl/yajl_version.h | 23 + nodes/CMakeLists.txt | 15 + .../CMakeLists.txt | 28 + nodes/attachment_registration_manager/main.cc | 25 + .../package/CMakeLists.txt | 4 + ...p-nano-attachment-registration-manager.cfg | 0 .../package/debug-conf.json | 11 + ...install-attachment-registration-manager.sh | 166 + .../package/service-conf.json | 21 + nodes/http_transaction_handler/CMakeLists.txt | 69 + nodes/http_transaction_handler/main.cc | 35 + .../package/CMakeLists.txt | 10 + ...tp-transaction-handler-conf-container.json | 45 + ...cp-nano-http-transaction-handler-conf.json | 38 + ...o-http-transaction-handler-debug-conf.json | 11 + .../cp-nano-http-transaction-handler.cfg | 0 .../install-http-transaction-handler.sh | 295 + .../package/k8s-log-file-handler.sh | 11 + nodes/orchestration/CMakeLists.txt | 145 + nodes/orchestration/main.cc | 89 + nodes/orchestration/package/CMakeLists.txt | 27 + nodes/orchestration/package/EULA.txt | 145 + .../Licenses-for-Third-Party-Components.txt | 67 + .../package/certificate/ngen.body.crt | 121 + .../certificate/public-keys/cloud-ngen.pem | 14 + .../certificate/public-keys/dev-i2.pem | 9 + .../package/certificate/public-keys/i2.pem | 14 + .../public-keys/public-key-general.pem | 14 + .../certificate/public-keys/stg-i2.pem | 14 + .../cp-nano-orchestration-conf.json | 21 + .../cp-nano-orchestration-debug-conf.json | 11 + .../package/configuration/orchestration.cfg | 4 + nodes/orchestration/package/cp-agent-info.sh | 256 + .../package/cp-agent-uninstall.sh | 172 + nodes/orchestration/package/cp-nano-cli.sh | 1594 + .../package/cp-nano-package-list | 30 + .../package/cpnano_debug/CMakeLists.txt | 6 + .../package/cpnano_debug/cpnano_debug.cc | 1465 + .../package/cpnano_json/CMakeLists.txt | 4 + .../package/cpnano_json/cpnano_json.cc | 35 + .../package/k8s-check-update-listener.sh | 79 + .../package/k8s-check-update-trigger.sh | 6 + .../package/orchestration_package.sh | 1053 + .../service/arm32_openwrt/nano_agent.init | 96 + .../package/service/smb/nano_agent.init | 101 + .../service/x86/ubuntu14/nano_agent.conf | 8 + .../service/x86/ubuntu14/nano_agent.init | 82 + .../service/x86/ubuntu16/nano_agent.service | 11 + .../package/watchdog/access_pre_init | 36 + .../wait-for-networking-inspection-modules.sh | 20 + nodes/orchestration/package/watchdog/watchdog | 860 + .../scripts/cp-nano-makefile-generator.sh | 369 + nodes/packaging.cmake | 47 + unit_test.cmake | 11 + 1353 files changed, 276290 insertions(+), 1 deletion(-) create mode 100644 CMakeLists.txt create mode 100644 CODE_OF_CONDUCT.md create mode 100644 CONTRIBUTING.md create mode 100644 LEXFO-CHP20221014-Report-Code_audit-OPEN-APPSEC-v1.2.pdf create mode 100644 LICENSE create mode 100644 SECURITY.md create mode 100644 attachments/CMakeLists.txt create mode 100755 attachments/kernel_modules/core/include/common_is/kdebug_flags.h create mode 100644 attachments/nginx/CMakeLists.txt create mode 100644 attachments/nginx/nginx_attachment_util/CMakeLists.txt create mode 100644 attachments/nginx/nginx_attachment_util/nginx_attachment_util.cc create mode 100644 attachments/nginx/nginx_attachment_util/nginx_attachment_util_ut/CMakeLists.txt create mode 100644 attachments/nginx/nginx_attachment_util/nginx_attachment_util_ut/nginx_attachment_util_ut.cc create mode 100644 build_system/CMakeLists.txt create mode 100644 build_system/charts/CMakeLists.txt create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/.helmignore create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/CHANGELOG.md create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/Chart.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/OWNERS create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/README.md create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/README.md.gotmpl create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/controller-custom-ingressclass-flags.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/daemonset-customconfig-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/daemonset-customnodeport-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/daemonset-extra-modules.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/daemonset-headers-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/daemonset-internal-lb-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/daemonset-nodeport-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/daemonset-podannotations-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/daemonset-tcp-udp-configMapNamespace-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/daemonset-tcp-udp-portNamePrefix-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/daemonset-tcp-udp-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/daemonset-tcp-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deamonset-default-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deamonset-metrics-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deamonset-psp-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deamonset-webhook-and-psp-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deamonset-webhook-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deployment-autoscaling-behavior-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deployment-autoscaling-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deployment-customconfig-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deployment-customnodeport-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deployment-default-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deployment-extra-modules.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deployment-headers-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deployment-internal-lb-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deployment-metrics-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deployment-nodeport-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deployment-podannotations-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deployment-psp-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deployment-tcp-udp-configMapNamespace-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deployment-tcp-udp-portNamePrefix-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deployment-tcp-udp-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deployment-tcp-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deployment-webhook-and-psp-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deployment-webhook-resources-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/ci/deployment-webhook-values.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/crds/crd-openappsec-custom-response.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/crds/crd-openappsec-exception.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/crds/crd-openappsec-log-trigger.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/crds/crd-openappsec-policy.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/crds/crd-openappsec-practice.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/crds/crd-openappsec-sources-identifier.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/crds/crd-openappsec-trusted-sources.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/_helpers.tpl create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/_params.tpl create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/admission-webhooks/job-patch/clusterrole.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/admission-webhooks/job-patch/clusterrolebinding.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/admission-webhooks/job-patch/job-createSecret.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/admission-webhooks/job-patch/job-patchWebhook.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/admission-webhooks/job-patch/psp.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/admission-webhooks/job-patch/role.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/admission-webhooks/job-patch/rolebinding.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/admission-webhooks/job-patch/serviceaccount.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/admission-webhooks/validating-webhook.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/appsec-learning-pvc.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/appsec-pvc.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/clusterrole.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/clusterrolebinding.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-configmap-addheaders.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-configmap-proxyheaders.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-configmap-tcp.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-configmap-udp.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-configmap.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-daemonset.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-deployment.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-hpa.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-ingressclass.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-keda.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-poddisruptionbudget.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-prometheusrules.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-psp.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-role.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-rolebinding.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-service-internal.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-service-metrics.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-service-webhook.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-service.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-serviceaccount.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-servicemonitor.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/controller-statefulset.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/default-backend-deployment.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/default-backend-hpa.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/default-backend-poddisruptionbudget.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/default-backend-psp.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/default-backend-role.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/default-backend-rolebinding.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/default-backend-service.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/default-backend-serviceaccount.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/default-policy.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/dh-param-secret.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/learning-deployment.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/templates/learning-services.yaml create mode 100644 build_system/charts/open-appsec-k8s-nginx-ingress/values.yaml create mode 100644 build_system/docker/CMakeLists.txt create mode 100644 build_system/docker/Dockerfile create mode 100644 build_system/docker/entry.sh create mode 100755 build_system/tools/packaging/makeself_wrapper.sh create mode 100644 components/CMakeLists.txt create mode 100644 components/attachment-intakers/CMakeLists.txt create mode 100755 components/attachment-intakers/attachment_registrator/CMakeLists.txt create mode 100755 components/attachment-intakers/attachment_registrator/attachment_registrator.cc create mode 100755 components/attachment-intakers/nginx_attachment/CMakeLists.txt create mode 100755 components/attachment-intakers/nginx_attachment/cidrs_data.cc create mode 100755 components/attachment-intakers/nginx_attachment/cidrs_data.h create mode 100755 components/attachment-intakers/nginx_attachment/intentional_failure.cc create mode 100755 components/attachment-intakers/nginx_attachment/intentional_failure.h create mode 100755 components/attachment-intakers/nginx_attachment/nginx_attachment.cc create mode 100755 components/attachment-intakers/nginx_attachment/nginx_attachment_config.cc create mode 100755 components/attachment-intakers/nginx_attachment/nginx_attachment_config.h create mode 100755 components/attachment-intakers/nginx_attachment/nginx_attachment_metric.cc create mode 100755 components/attachment-intakers/nginx_attachment/nginx_attachment_opaque.cc create mode 100755 components/attachment-intakers/nginx_attachment/nginx_attachment_opaque.h create mode 100755 components/attachment-intakers/nginx_attachment/nginx_intaker_metric.cc create mode 100755 components/attachment-intakers/nginx_attachment/nginx_parser.cc create mode 100755 components/attachment-intakers/nginx_attachment/nginx_parser.h create mode 100755 components/attachment-intakers/nginx_attachment/user_identifiers_config.cc create mode 100755 components/generic_rulebase/CMakeLists.txt create mode 100755 components/generic_rulebase/assets_config.cc create mode 100755 components/generic_rulebase/evaluators/CMakeLists.txt create mode 100755 components/generic_rulebase/evaluators/asset_eval.cc create mode 100755 components/generic_rulebase/evaluators/connection_eval.cc create mode 100755 components/generic_rulebase/evaluators/http_transaction_data_eval.cc create mode 100755 components/generic_rulebase/evaluators/parameter_eval.cc create mode 100755 components/generic_rulebase/evaluators/practice_eval.cc create mode 100755 components/generic_rulebase/evaluators/query_eval.cc create mode 100755 components/generic_rulebase/evaluators/trigger_eval.cc create mode 100755 components/generic_rulebase/evaluators/zone_eval.cc create mode 100755 components/generic_rulebase/generic_rulebase.cc create mode 100755 components/generic_rulebase/generic_rulebase_context.cc create mode 100755 components/generic_rulebase/match_query.cc create mode 100755 components/generic_rulebase/parameters_config.cc create mode 100755 components/generic_rulebase/rulebase_config.cc create mode 100755 components/generic_rulebase/triggers_config.cc create mode 100755 components/generic_rulebase/zone.cc create mode 100755 components/generic_rulebase/zones_config.cc create mode 100644 components/gradual_deployment/CMakeLists.txt create mode 100644 components/gradual_deployment/gradual_deployment.cc create mode 100644 components/gradual_deployment/gradual_deployment_ut/CMakeLists.txt create mode 100644 components/gradual_deployment/gradual_deployment_ut/gradual_deployment_ut.cc create mode 100755 components/health_check_manager/CMakeLists.txt create mode 100755 components/health_check_manager/health_check_manager.cc create mode 100755 components/health_check_manager/health_check_manager_ut/CMakeLists.txt create mode 100755 components/health_check_manager/health_check_manager_ut/health_check_manager_ut.cc create mode 100644 components/http_manager/CMakeLists.txt create mode 100755 components/http_manager/http_manager.cc create mode 100644 components/http_manager/http_manager_opaque.cc create mode 100644 components/http_manager/http_manager_opaque.h create mode 100755 components/http_transaction_data/CMakeLists.txt create mode 100644 components/http_transaction_data/http_transaction_data.cc create mode 100755 components/http_transaction_data/http_transaction_data_ut/CMakeLists.txt create mode 100644 components/http_transaction_data/http_transaction_data_ut/http_transaction_data_ut.cc create mode 100755 components/include/WaapEnums.h create mode 100755 components/include/attachment_registrator.h create mode 100755 components/include/byteorder.h create mode 100644 components/include/details_resolver.h create mode 100755 components/include/downloader.h create mode 100755 components/include/external_sdk_server.h create mode 100755 components/include/generic_rulebase/asset.h create mode 100755 components/include/generic_rulebase/assets_config.h create mode 100755 components/include/generic_rulebase/evaluators/asset_eval.h create mode 100755 components/include/generic_rulebase/evaluators/connection_eval.h create mode 100755 components/include/generic_rulebase/evaluators/http_transaction_data_eval.h create mode 100755 components/include/generic_rulebase/evaluators/parameter_eval.h create mode 100755 components/include/generic_rulebase/evaluators/practice_eval.h create mode 100755 components/include/generic_rulebase/evaluators/query_eval.h create mode 100755 components/include/generic_rulebase/evaluators/trigger_eval.h create mode 100755 components/include/generic_rulebase/evaluators/zone_eval.h create mode 100755 components/include/generic_rulebase/generic_rulebase.h create mode 100755 components/include/generic_rulebase/generic_rulebase_context.h create mode 100755 components/include/generic_rulebase/generic_rulebase_utils.h create mode 100755 components/include/generic_rulebase/match_query.h create mode 100755 components/include/generic_rulebase/parameters_config.h create mode 100755 components/include/generic_rulebase/rulebase_config.h create mode 100755 components/include/generic_rulebase/triggers_config.h create mode 100755 components/include/generic_rulebase/zone.h create mode 100755 components/include/generic_rulebase/zones_config.h create mode 100644 components/include/gradual_deployment.h create mode 100755 components/include/health_check_manager.h create mode 100755 components/include/health_checker.h create mode 100755 components/include/http_event_impl/filter_verdict.h create mode 100755 components/include/http_event_impl/i_http_event_impl.h create mode 100755 components/include/http_inspection_events.h create mode 100755 components/include/http_manager.h create mode 100644 components/include/http_transaction_common.h create mode 100755 components/include/http_transaction_data.h create mode 100755 components/include/hybrid_mode_telemetry.h create mode 100644 components/include/i_details_resolver.h create mode 100755 components/include/i_downloader.h create mode 100755 components/include/i_external_sdk_server.h create mode 100755 components/include/i_generic_rulebase.h create mode 100644 components/include/i_gradual_deployment.h create mode 100755 components/include/i_http_manager.h create mode 100755 components/include/i_k8s_policy_gen.h create mode 100755 components/include/i_manifest_controller.h create mode 100755 components/include/i_orchestration_status.h create mode 100755 components/include/i_orchestration_tools.h create mode 100755 components/include/i_package_handler.h create mode 100755 components/include/i_pm_scan.h create mode 100755 components/include/i_service_controller.h create mode 100755 components/include/i_static_resources_handler.h create mode 100755 components/include/i_update_communication.h create mode 100755 components/include/i_waap_telemetry.h create mode 100755 components/include/ip_utilities.h create mode 100644 components/include/k8s_policy_gen.h create mode 100755 components/include/manifest_controller.h create mode 100755 components/include/manifest_diff_calculator.h create mode 100755 components/include/manifest_handler.h create mode 100755 components/include/messaging_downloader_client.h create mode 100755 components/include/messaging_downloader_server.h create mode 100755 components/include/mock/mock_nginx_attachment.h create mode 100755 components/include/nginx_attachment.h create mode 100755 components/include/nginx_attachment_metric.h create mode 100755 components/include/nginx_intaker_metric.h create mode 100755 components/include/orchestration_comp.h create mode 100755 components/include/orchestration_status.h create mode 100755 components/include/orchestration_tools.h create mode 100755 components/include/orchestrator/data.h create mode 100644 components/include/orchestrator/rest_api/get_resource_file.h create mode 100644 components/include/orchestrator/rest_api/orchestration_check_update.h create mode 100755 components/include/package.h create mode 100755 components/include/package_handler.h create mode 100755 components/include/packet.h create mode 100755 components/include/pending_key.h create mode 100644 components/include/pm_hook.h create mode 100755 components/include/report_messaging.h create mode 100755 components/include/service_controller.h create mode 100755 components/include/service_details.h create mode 100755 components/include/signal_handler.h create mode 100755 components/include/telemetry.h create mode 100755 components/include/transaction_table_metric.h create mode 100644 components/include/type_defs.h create mode 100755 components/include/update_communication.h create mode 100755 components/include/url_parser.h create mode 100755 components/include/user_identifiers_config.h create mode 100755 components/include/waap.h create mode 100755 components/messaging_downloader/CMakeLists.txt create mode 100755 components/messaging_downloader/messaging_downloader_client.cc create mode 100755 components/messaging_downloader/messaging_downloader_server.cc create mode 100755 components/messaging_downloader/messaging_downloader_ut/CMakeLists.txt create mode 100755 components/messaging_downloader/messaging_downloader_ut/downloader_client_ut/CMakeLists.txt create mode 100755 components/messaging_downloader/messaging_downloader_ut/downloader_client_ut/downloader_client_ut.cc create mode 100755 components/messaging_downloader/messaging_downloader_ut/downloader_server_ut/CMakeLists.txt create mode 100755 components/messaging_downloader/messaging_downloader_ut/downloader_server_ut/downloader_server_ut.cc create mode 100644 components/packet/CMakeLists.txt create mode 100755 components/packet/packet.cc create mode 100644 components/packet/packet_ut/CMakeLists.txt create mode 100755 components/packet/packet_ut/packet_ut.cc create mode 100755 components/pending_key/CMakeLists.txt create mode 100755 components/pending_key/pending_key.cc create mode 100755 components/pending_key/pending_key_ut/CMakeLists.txt create mode 100755 components/pending_key/pending_key_ut/pending_key_ut.cc create mode 100644 components/report_messaging/CMakeLists.txt create mode 100755 components/report_messaging/report_messaging.cc create mode 100644 components/report_messaging/report_messaging_ut/CMakeLists.txt create mode 100644 components/report_messaging/report_messaging_ut/report_messaging_ut.cc create mode 100644 components/security_apps/CMakeLists.txt create mode 100755 components/security_apps/orchestration/CMakeLists.txt create mode 100644 components/security_apps/orchestration/details_resolver/CMakeLists.txt create mode 100644 components/security_apps/orchestration/details_resolver/details_resolver.cc create mode 100755 components/security_apps/orchestration/details_resolver/details_resolver_handlers/checkpoint_product_handlers.h create mode 100755 components/security_apps/orchestration/details_resolver/details_resolver_handlers/details_resolver_impl.h create mode 100755 components/security_apps/orchestration/details_resolver/details_resolving_handler.cc create mode 100755 components/security_apps/orchestration/details_resolver/details_resolving_handler.h create mode 100755 components/security_apps/orchestration/downloader/CMakeLists.txt create mode 100755 components/security_apps/orchestration/downloader/curl_client.cc create mode 100755 components/security_apps/orchestration/downloader/curl_client.h create mode 100755 components/security_apps/orchestration/downloader/downloader.cc create mode 100755 components/security_apps/orchestration/downloader/downloader_ut/CMakeLists.txt create mode 100755 components/security_apps/orchestration/downloader/downloader_ut/downloader_ut.cc create mode 100755 components/security_apps/orchestration/downloader/http_client.cc create mode 100755 components/security_apps/orchestration/downloader/http_client.h create mode 100755 components/security_apps/orchestration/downloader/https_client.cc create mode 100755 components/security_apps/orchestration/health_check/CMakeLists.txt create mode 100755 components/security_apps/orchestration/health_check/health_check.cc create mode 100755 components/security_apps/orchestration/health_check/health_check_ut/CMakeLists.txt create mode 100755 components/security_apps/orchestration/health_check/health_check_ut/health_check_ut.cc create mode 100755 components/security_apps/orchestration/hybrid_mode_telemetry.cc create mode 100755 components/security_apps/orchestration/include/fog_authenticator.h create mode 100755 components/security_apps/orchestration/include/fog_communication.h create mode 100755 components/security_apps/orchestration/include/get_status_rest.h create mode 100755 components/security_apps/orchestration/include/hybrid_communication.h create mode 100755 components/security_apps/orchestration/include/local_communication.h create mode 100644 components/security_apps/orchestration/include/mock/mock_details_resolver.h create mode 100755 components/security_apps/orchestration/include/mock/mock_downloader.h create mode 100755 components/security_apps/orchestration/include/mock/mock_manifest_controller.h create mode 100755 components/security_apps/orchestration/include/mock/mock_messaging_downloader.h create mode 100644 components/security_apps/orchestration/include/mock/mock_orchestration_status.h create mode 100755 components/security_apps/orchestration/include/mock/mock_orchestration_tools.h create mode 100755 components/security_apps/orchestration/include/mock/mock_package_handler.h create mode 100755 components/security_apps/orchestration/include/mock/mock_service_controller.h create mode 100755 components/security_apps/orchestration/include/mock/mock_update_communication.h create mode 100755 components/security_apps/orchestration/include/orchestration_policy.h create mode 100644 components/security_apps/orchestration/k8s_policy_gen/CMakeLists.txt create mode 100644 components/security_apps/orchestration/k8s_policy_gen/include/appsec_practice_section.h create mode 100644 components/security_apps/orchestration/k8s_policy_gen/include/exceptions_section.h create mode 100644 components/security_apps/orchestration/k8s_policy_gen/include/ingress_data.h create mode 100644 components/security_apps/orchestration/k8s_policy_gen/include/k8s_policy_common.h create mode 100644 components/security_apps/orchestration/k8s_policy_gen/include/rules_config_section.h create mode 100644 components/security_apps/orchestration/k8s_policy_gen/include/settings_section.h create mode 100644 components/security_apps/orchestration/k8s_policy_gen/include/snort_section.h create mode 100644 components/security_apps/orchestration/k8s_policy_gen/include/triggers_section.h create mode 100755 components/security_apps/orchestration/k8s_policy_gen/include/trusted_sources_section.h create mode 100644 components/security_apps/orchestration/k8s_policy_gen/k8s_policy_gen.cc create mode 100755 components/security_apps/orchestration/manifest_controller/CMakeLists.txt create mode 100755 components/security_apps/orchestration/manifest_controller/manifest_controller.cc create mode 100755 components/security_apps/orchestration/manifest_controller/manifest_controller_ut/CMakeLists.txt create mode 100755 components/security_apps/orchestration/manifest_controller/manifest_controller_ut/manifest_controller_ut.cc create mode 100755 components/security_apps/orchestration/manifest_controller/manifest_diff_calculator.cc create mode 100755 components/security_apps/orchestration/manifest_controller/manifest_handler.cc create mode 100755 components/security_apps/orchestration/modules/CMakeLists.txt create mode 100755 components/security_apps/orchestration/modules/data.cc create mode 100755 components/security_apps/orchestration/modules/modules_ut/CMakeLists.txt create mode 100755 components/security_apps/orchestration/modules/modules_ut/data_ut.cc create mode 100755 components/security_apps/orchestration/modules/modules_ut/orchestration_policy_ut.cc create mode 100755 components/security_apps/orchestration/modules/modules_ut/orchestration_status_ut.cc create mode 100755 components/security_apps/orchestration/modules/modules_ut/package_ut.cc create mode 100755 components/security_apps/orchestration/modules/modules_ut/url_parser_ut.cc create mode 100755 components/security_apps/orchestration/modules/orchestration_policy.cc create mode 100755 components/security_apps/orchestration/modules/orchestration_status.cc create mode 100755 components/security_apps/orchestration/modules/package.cc create mode 100755 components/security_apps/orchestration/modules/url_parser.cc create mode 100755 components/security_apps/orchestration/orchestration_comp.cc create mode 100755 components/security_apps/orchestration/orchestration_tools/CMakeLists.txt create mode 100755 components/security_apps/orchestration/orchestration_tools/orchestration_tools.cc create mode 100755 components/security_apps/orchestration/orchestration_tools/orchestration_tools_ut/CMakeLists.txt create mode 100755 components/security_apps/orchestration/orchestration_tools/orchestration_tools_ut/orchestration_tools_ut.cc create mode 100755 components/security_apps/orchestration/orchestration_ut/CMakeLists.txt create mode 100644 components/security_apps/orchestration/orchestration_ut/orchestration_multitenant_ut.cc create mode 100755 components/security_apps/orchestration/orchestration_ut/orchestration_ut.cc create mode 100755 components/security_apps/orchestration/package_handler/CMakeLists.txt create mode 100755 components/security_apps/orchestration/package_handler/package_handler.cc create mode 100755 components/security_apps/orchestration/package_handler/package_handler_ut/CMakeLists.txt create mode 100755 components/security_apps/orchestration/package_handler/package_handler_ut/package_handler_ut.cc create mode 100755 components/security_apps/orchestration/service_controller/CMakeLists.txt create mode 100755 components/security_apps/orchestration/service_controller/service_controller.cc create mode 100755 components/security_apps/orchestration/service_controller/service_controller_ut/CMakeLists.txt create mode 100755 components/security_apps/orchestration/service_controller/service_controller_ut/service_controller_ut.cc create mode 100755 components/security_apps/orchestration/update_communication/CMakeLists.txt create mode 100755 components/security_apps/orchestration/update_communication/fog_authenticator.cc create mode 100755 components/security_apps/orchestration/update_communication/fog_communication.cc create mode 100755 components/security_apps/orchestration/update_communication/hybrid_communication.cc create mode 100755 components/security_apps/orchestration/update_communication/local_communication.cc create mode 100755 components/security_apps/orchestration/update_communication/update_communication.cc create mode 100755 components/security_apps/orchestration/update_communication/update_communication_ut/CMakeLists.txt create mode 100755 components/security_apps/orchestration/update_communication/update_communication_ut/local_communication_ut.cc create mode 100755 components/security_apps/waap/CMakeLists.txt create mode 100755 components/security_apps/waap/first_request_object.cc create mode 100755 components/security_apps/waap/first_request_object.h create mode 100755 components/security_apps/waap/include/WaapDefines.h create mode 100755 components/security_apps/waap/include/i_deepAnalyzer.h create mode 100755 components/security_apps/waap/include/i_ignoreSources.h create mode 100755 components/security_apps/waap/include/i_indicatorsFilter.h create mode 100755 components/security_apps/waap/include/i_serialize.h create mode 100755 components/security_apps/waap/include/i_transaction.h create mode 100755 components/security_apps/waap/include/i_waapConfig.h create mode 100755 components/security_apps/waap/include/i_waap_asset_state.h create mode 100755 components/security_apps/waap/include/picojson.h create mode 100755 components/security_apps/waap/include/reputation_features_events.h create mode 100755 components/security_apps/waap/reputation/CMakeLists.txt create mode 100755 components/security_apps/waap/reputation/reputation_features_agg.cc create mode 100755 components/security_apps/waap/reputation/reputation_features_agg.h create mode 100755 components/security_apps/waap/resources/1.data create mode 100755 components/security_apps/waap/resources/2.data create mode 100755 components/security_apps/waap/resources/8.data create mode 100755 components/security_apps/waap/waap_clib/AutonomousSecurityDecision.cc create mode 100755 components/security_apps/waap/waap_clib/AutonomousSecurityDecision.h create mode 100755 components/security_apps/waap/waap_clib/BehaviorAnalysis.cc create mode 100755 components/security_apps/waap/waap_clib/BehaviorAnalysis.h create mode 100755 components/security_apps/waap/waap_clib/CMakeLists.txt create mode 100755 components/security_apps/waap/waap_clib/CidrMatch.cc create mode 100755 components/security_apps/waap/waap_clib/CidrMatch.h create mode 100755 components/security_apps/waap/waap_clib/ConfidenceCalculator.cc create mode 100755 components/security_apps/waap/waap_clib/ConfidenceCalculator.h create mode 100755 components/security_apps/waap/waap_clib/ConfidenceFile.cc create mode 100755 components/security_apps/waap/waap_clib/ConfidenceFile.h create mode 100755 components/security_apps/waap/waap_clib/ContentTypeParser.cc create mode 100755 components/security_apps/waap/waap_clib/ContentTypeParser.h create mode 100644 components/security_apps/waap/waap_clib/Csrf.cc create mode 100644 components/security_apps/waap/waap_clib/Csrf.h create mode 100755 components/security_apps/waap/waap_clib/CsrfDecision.cc create mode 100755 components/security_apps/waap/waap_clib/CsrfDecision.h create mode 100644 components/security_apps/waap/waap_clib/CsrfPolicy.cc create mode 100644 components/security_apps/waap/waap_clib/CsrfPolicy.h create mode 100755 components/security_apps/waap/waap_clib/D2Main.cc create mode 100755 components/security_apps/waap/waap_clib/D2Main.h create mode 100755 components/security_apps/waap/waap_clib/DataTypes.h create mode 100755 components/security_apps/waap/waap_clib/DecisionFactory.cc create mode 100755 components/security_apps/waap/waap_clib/DecisionFactory.h create mode 100755 components/security_apps/waap/waap_clib/DecisionType.h create mode 100755 components/security_apps/waap/waap_clib/DeepAnalyzer.cc create mode 100755 components/security_apps/waap/waap_clib/DeepAnalyzer.h create mode 100755 components/security_apps/waap/waap_clib/DeepParser.cc create mode 100755 components/security_apps/waap/waap_clib/DeepParser.h create mode 100755 components/security_apps/waap/waap_clib/ErrorDisclosureDecision.cc create mode 100755 components/security_apps/waap/waap_clib/ErrorDisclosureDecision.h create mode 100644 components/security_apps/waap/waap_clib/ErrorLimiting.cc create mode 100644 components/security_apps/waap/waap_clib/ErrorLimiting.h create mode 100755 components/security_apps/waap/waap_clib/ErrorLimitingDecision.cc create mode 100755 components/security_apps/waap/waap_clib/ErrorLimitingDecision.h create mode 100755 components/security_apps/waap/waap_clib/FpMitigation.cc create mode 100755 components/security_apps/waap/waap_clib/FpMitigation.h create mode 100755 components/security_apps/waap/waap_clib/IndicatorsFilterBase.cc create mode 100755 components/security_apps/waap/waap_clib/IndicatorsFilterBase.h create mode 100755 components/security_apps/waap/waap_clib/IndicatorsFiltersManager.cc create mode 100755 components/security_apps/waap/waap_clib/IndicatorsFiltersManager.h create mode 100755 components/security_apps/waap/waap_clib/KeyStack.cc create mode 100755 components/security_apps/waap/waap_clib/KeyStack.h create mode 100755 components/security_apps/waap/waap_clib/KeywordIndicatorFilter.cc create mode 100755 components/security_apps/waap/waap_clib/KeywordIndicatorFilter.h create mode 100755 components/security_apps/waap/waap_clib/KeywordTypeValidator.cc create mode 100755 components/security_apps/waap/waap_clib/KeywordTypeValidator.h create mode 100644 components/security_apps/waap/waap_clib/LogGenWrapper.cc create mode 100644 components/security_apps/waap/waap_clib/LogGenWrapper.h create mode 100755 components/security_apps/waap/waap_clib/OpenRedirectDecision.cc create mode 100755 components/security_apps/waap/waap_clib/OpenRedirectDecision.h create mode 100644 components/security_apps/waap/waap_clib/PHPSerializedDataParser.cc create mode 100644 components/security_apps/waap/waap_clib/PHPSerializedDataParser.h create mode 100755 components/security_apps/waap/waap_clib/ParserBase.cc create mode 100755 components/security_apps/waap/waap_clib/ParserBase.h create mode 100755 components/security_apps/waap/waap_clib/ParserBinary.cc create mode 100755 components/security_apps/waap/waap_clib/ParserBinary.h create mode 100755 components/security_apps/waap/waap_clib/ParserConfluence.cc create mode 100755 components/security_apps/waap/waap_clib/ParserConfluence.h create mode 100755 components/security_apps/waap/waap_clib/ParserDelimiter.cc create mode 100755 components/security_apps/waap/waap_clib/ParserDelimiter.h create mode 100755 components/security_apps/waap/waap_clib/ParserHTML.cc create mode 100755 components/security_apps/waap/waap_clib/ParserHTML.h create mode 100755 components/security_apps/waap/waap_clib/ParserHdrValue.cc create mode 100755 components/security_apps/waap/waap_clib/ParserHdrValue.h create mode 100755 components/security_apps/waap/waap_clib/ParserJson.cc create mode 100755 components/security_apps/waap/waap_clib/ParserJson.h create mode 100755 components/security_apps/waap/waap_clib/ParserMultipartForm.cc create mode 100755 components/security_apps/waap/waap_clib/ParserMultipartForm.h create mode 100755 components/security_apps/waap/waap_clib/ParserRaw.cc create mode 100755 components/security_apps/waap/waap_clib/ParserRaw.h create mode 100755 components/security_apps/waap/waap_clib/ParserUrlEncode.cc create mode 100755 components/security_apps/waap/waap_clib/ParserUrlEncode.h create mode 100755 components/security_apps/waap/waap_clib/ParserXML.cc create mode 100755 components/security_apps/waap/waap_clib/ParserXML.h create mode 100755 components/security_apps/waap/waap_clib/PatternMatcher.cc create mode 100755 components/security_apps/waap/waap_clib/PatternMatcher.h create mode 100755 components/security_apps/waap/waap_clib/RateLimiter.cc create mode 100755 components/security_apps/waap/waap_clib/RateLimiter.h create mode 100755 components/security_apps/waap/waap_clib/RateLimiting.cc create mode 100755 components/security_apps/waap/waap_clib/RateLimiting.h create mode 100755 components/security_apps/waap/waap_clib/RateLimitingDecision.cc create mode 100755 components/security_apps/waap/waap_clib/RateLimitingDecision.h create mode 100755 components/security_apps/waap/waap_clib/ScanResult.cc create mode 100755 components/security_apps/waap/waap_clib/ScanResult.h create mode 100755 components/security_apps/waap/waap_clib/ScannerDetector.cc create mode 100755 components/security_apps/waap/waap_clib/ScannersDetector.h create mode 100755 components/security_apps/waap/waap_clib/ScoreBuilder.cc create mode 100755 components/security_apps/waap/waap_clib/ScoreBuilder.h create mode 100644 components/security_apps/waap/waap_clib/SecurityHeadersPolicy.cc create mode 100644 components/security_apps/waap/waap_clib/SecurityHeadersPolicy.h create mode 100755 components/security_apps/waap/waap_clib/Serializator.cc create mode 100755 components/security_apps/waap/waap_clib/Signatures.cc create mode 100755 components/security_apps/waap/waap_clib/Signatures.h create mode 100755 components/security_apps/waap/waap_clib/SingleDecision.cc create mode 100755 components/security_apps/waap/waap_clib/SingleDecision.h create mode 100755 components/security_apps/waap/waap_clib/SyncLearningNotification.cc create mode 100755 components/security_apps/waap/waap_clib/SyncLearningNotification.h create mode 100755 components/security_apps/waap/waap_clib/Telemetry.cc create mode 100755 components/security_apps/waap/waap_clib/TrustedSources.cc create mode 100755 components/security_apps/waap/waap_clib/TrustedSources.h create mode 100755 components/security_apps/waap/waap_clib/TrustedSourcesConfidence.cc create mode 100755 components/security_apps/waap/waap_clib/TrustedSourcesConfidence.h create mode 100755 components/security_apps/waap/waap_clib/TuningDecision.cc create mode 100755 components/security_apps/waap/waap_clib/TuningDecisions.h create mode 100755 components/security_apps/waap/waap_clib/TypeIndicatorsFilter.cc create mode 100755 components/security_apps/waap/waap_clib/TypeIndicatorsFilter.h create mode 100755 components/security_apps/waap/waap_clib/UserLimitsDecision.cc create mode 100755 components/security_apps/waap/waap_clib/UserLimitsDecision.h create mode 100644 components/security_apps/waap/waap_clib/UserLimitsPolicy.cc create mode 100644 components/security_apps/waap/waap_clib/UserLimitsPolicy.h create mode 100755 components/security_apps/waap/waap_clib/WaapAssetState.cc create mode 100755 components/security_apps/waap/waap_clib/WaapAssetState.h create mode 100755 components/security_apps/waap/waap_clib/WaapAssetStatesManager.cc create mode 100755 components/security_apps/waap/waap_clib/WaapAssetStatesManager.h create mode 100755 components/security_apps/waap/waap_clib/WaapConfigApi.cc create mode 100755 components/security_apps/waap/waap_clib/WaapConfigApi.h create mode 100755 components/security_apps/waap/waap_clib/WaapConfigApplication.cc create mode 100755 components/security_apps/waap/waap_clib/WaapConfigApplication.h create mode 100755 components/security_apps/waap/waap_clib/WaapConfigBase.cc create mode 100755 components/security_apps/waap/waap_clib/WaapConfigBase.h create mode 100644 components/security_apps/waap/waap_clib/WaapConversions.cc create mode 100644 components/security_apps/waap/waap_clib/WaapConversions.h create mode 100755 components/security_apps/waap/waap_clib/WaapDecision.cc create mode 100755 components/security_apps/waap/waap_clib/WaapDecision.h create mode 100755 components/security_apps/waap/waap_clib/WaapErrorDisclosurePolicy.cc create mode 100755 components/security_apps/waap/waap_clib/WaapErrorDisclosurePolicy.h create mode 100644 components/security_apps/waap/waap_clib/WaapKeywords.cc create mode 100644 components/security_apps/waap/waap_clib/WaapKeywords.h create mode 100755 components/security_apps/waap/waap_clib/WaapOpenRedirect.cc create mode 100755 components/security_apps/waap/waap_clib/WaapOpenRedirect.h create mode 100755 components/security_apps/waap/waap_clib/WaapOpenRedirectPolicy.cc create mode 100755 components/security_apps/waap/waap_clib/WaapOpenRedirectPolicy.h create mode 100755 components/security_apps/waap/waap_clib/WaapOverride.cc create mode 100755 components/security_apps/waap/waap_clib/WaapOverride.h create mode 100755 components/security_apps/waap/waap_clib/WaapOverrideFunctor.cc create mode 100755 components/security_apps/waap/waap_clib/WaapOverrideFunctor.h create mode 100755 components/security_apps/waap/waap_clib/WaapParameters.cc create mode 100755 components/security_apps/waap/waap_clib/WaapParameters.h create mode 100755 components/security_apps/waap/waap_clib/WaapRegexPreconditions.cc create mode 100755 components/security_apps/waap/waap_clib/WaapRegexPreconditions.h create mode 100644 components/security_apps/waap/waap_clib/WaapResponseInjectReasons.cc create mode 100644 components/security_apps/waap/waap_clib/WaapResponseInjectReasons.h create mode 100644 components/security_apps/waap/waap_clib/WaapResponseInspectReasons.cc create mode 100644 components/security_apps/waap/waap_clib/WaapResponseInspectReasons.h create mode 100644 components/security_apps/waap/waap_clib/WaapResultJson.cc create mode 100644 components/security_apps/waap/waap_clib/WaapResultJson.h create mode 100644 components/security_apps/waap/waap_clib/WaapSampleValue.cc create mode 100644 components/security_apps/waap/waap_clib/WaapSampleValue.h create mode 100755 components/security_apps/waap/waap_clib/WaapScanner.cc create mode 100644 components/security_apps/waap/waap_clib/WaapScanner.h create mode 100644 components/security_apps/waap/waap_clib/WaapScores.cc create mode 100644 components/security_apps/waap/waap_clib/WaapScores.h create mode 100755 components/security_apps/waap/waap_clib/WaapTrigger.cc create mode 100755 components/security_apps/waap/waap_clib/WaapTrigger.h create mode 100755 components/security_apps/waap/waap_clib/WaapValueStatsAnalyzer.cc create mode 100755 components/security_apps/waap/waap_clib/WaapValueStatsAnalyzer.h create mode 100755 components/security_apps/waap/waap_clib/Waf2Engine.cc create mode 100755 components/security_apps/waap/waap_clib/Waf2Engine.h create mode 100755 components/security_apps/waap/waap_clib/Waf2EngineGetters.cc create mode 100755 components/security_apps/waap/waap_clib/Waf2Regex.cc create mode 100755 components/security_apps/waap/waap_clib/Waf2Regex.h create mode 100755 components/security_apps/waap/waap_clib/Waf2Util.cc create mode 100755 components/security_apps/waap/waap_clib/Waf2Util.h create mode 100755 components/security_apps/waap/waap_clib/lru_cache_map.h create mode 100755 components/security_apps/waap/waap_clib/lru_cache_set.h create mode 100755 components/security_apps/waap/waap_clib/waf2_reporting.h create mode 100755 components/security_apps/waap/waap_component.cc create mode 100755 components/security_apps/waap/waap_component_impl.cc create mode 100755 components/security_apps/waap/waap_component_impl.h create mode 100755 components/signal_handler/CMakeLists.txt create mode 100755 components/signal_handler/signal_handler.cc create mode 100644 components/utils/CMakeLists.txt create mode 100755 components/utils/ip_utilities/CMakeLists.txt create mode 100644 components/utils/ip_utilities/ip_utilities.cc create mode 100644 components/utils/pm/CMakeLists.txt create mode 100755 components/utils/pm/debugpm.cc create mode 100755 components/utils/pm/debugpm.h create mode 100644 components/utils/pm/general_adaptor.cc create mode 100644 components/utils/pm/general_adaptor.h create mode 100644 components/utils/pm/kiss_hash.cc create mode 100644 components/utils/pm/kiss_hash.h create mode 100644 components/utils/pm/kiss_patterns.cc create mode 100644 components/utils/pm/kiss_patterns.h create mode 100644 components/utils/pm/kiss_pm_stats.cc create mode 100644 components/utils/pm/kiss_pm_stats.h create mode 100644 components/utils/pm/kiss_thin_nfa.cc create mode 100644 components/utils/pm/kiss_thin_nfa_analyze.cc create mode 100644 components/utils/pm/kiss_thin_nfa_base.h create mode 100644 components/utils/pm/kiss_thin_nfa_build.cc create mode 100644 components/utils/pm/kiss_thin_nfa_compile.cc create mode 100644 components/utils/pm/kiss_thin_nfa_impl.h create mode 100644 components/utils/pm/lss_example.txt create mode 100644 components/utils/pm/pm_adaptor.cc create mode 100644 components/utils/pm/pm_adaptor.h create mode 100644 components/utils/pm/pm_hook.cc create mode 100644 components/utils/pm/pm_ut/CMakeLists.txt create mode 100644 components/utils/pm/pm_ut/pm_pat_ut.cc create mode 100644 components/utils/pm/pm_ut/pm_scan_ut.cc create mode 100644 core/CMakeLists.txt create mode 100644 core/README.md create mode 100755 core/agent_core_utilities/CMakeLists.txt create mode 100755 core/agent_core_utilities/agent_core_utilities.cc create mode 100644 core/agent_core_utilities/agent_core_utilities_ut/CMakeLists.txt create mode 100644 core/agent_core_utilities/agent_core_utilities_ut/agent_core_utilities_ut.cc create mode 100755 core/agent_details/CMakeLists.txt create mode 100755 core/agent_details/agent_details.cc create mode 100644 core/agent_details/agent_details_ut/CMakeLists.txt create mode 100644 core/agent_details/agent_details_ut/agent_details_ut.cc create mode 100644 core/agent_details_reporter/CMakeLists.txt create mode 100644 core/agent_details_reporter/agent_details_report.cc create mode 100644 core/agent_details_reporter/agent_details_reporter.cc create mode 100644 core/agent_details_reporter/agent_details_reporter_ut/CMakeLists.txt create mode 100644 core/agent_details_reporter/agent_details_reporter_ut/agent_details_reporter_ut.cc create mode 100644 core/attachments/CMakeLists.txt create mode 100644 core/attachments/http_configuration/CMakeLists.txt create mode 100644 core/attachments/http_configuration/http_configuration.cc create mode 100644 core/attachments/http_configuration/http_configuration_ut/CMakeLists.txt create mode 100644 core/attachments/http_configuration/http_configuration_ut/http_configuration_ut.cc create mode 100644 core/buffers/CMakeLists.txt create mode 100755 core/buffers/buffer.cc create mode 100644 core/buffers/buffer_eval.cc create mode 100644 core/buffers/buffers_ut/CMakeLists.txt create mode 100644 core/buffers/buffers_ut/buffer_eval_ut.cc create mode 100644 core/buffers/buffers_ut/buffers_ut.cc create mode 100755 core/buffers/char_iterator.cc create mode 100755 core/buffers/data_container.cc create mode 100755 core/buffers/segment.cc create mode 100755 core/compression/CMakeLists.txt create mode 100755 core/compression/compression_utils.cc create mode 100755 core/compression/compression_utils_ut/CMakeLists.txt create mode 100755 core/compression/compression_utils_ut/compression_utils_ut.cc create mode 100644 core/compression/compression_utils_ut/test_files/chunk_sized_compressed_file.gz create mode 100755 core/compression/compression_utils_ut/test_files/chunk_sized_compressed_file.zz create mode 100644 core/compression/compression_utils_ut/test_files/chunk_sized_string create mode 100755 core/compression/compression_utils_ut/test_files/multiple_chunk_sized_compressed_file.gz create mode 100755 core/compression/compression_utils_ut/test_files/multiple_chunk_sized_compressed_file.zz create mode 100755 core/compression/compression_utils_ut/test_files/multiple_chunk_sized_string create mode 100644 core/compression/compression_utils_ut/test_files/out_of_output_space_test_compressed_file.gz create mode 100644 core/config/CMakeLists.txt create mode 100644 core/config/config.cc create mode 100644 core/config/config_globals.cc create mode 100644 core/config/config_specific.cc create mode 100755 core/config/include/profile_settings.h create mode 100644 core/connkey/CMakeLists.txt create mode 100755 core/connkey/connkey.cc create mode 100644 core/connkey/connkey_eval.cc create mode 100644 core/core_ut/CMakeLists.txt create mode 100644 core/core_ut/cache_ut.cc create mode 100644 core/core_ut/common_ut.cc create mode 100644 core/core_ut/enum_array_ut.cc create mode 100644 core/core_ut/enum_range_ut.cc create mode 100755 core/core_ut/maybe_res_ut.cc create mode 100644 core/core_ut/tostring_ut.cc create mode 100644 core/core_ut/virtual_container_ut.cc create mode 100644 core/cptest/CMakeLists.txt create mode 100755 core/cptest/cptest.cc create mode 100755 core/cptest/cptest_data_buf.cc create mode 100755 core/cptest/cptest_tcppacket.cc create mode 100644 core/cptest/cptest_ut/CMakeLists.txt create mode 100755 core/cptest/cptest_ut/cptest_packet_ut.cc create mode 100644 core/cptest/cptest_ut/cptest_ut.cc create mode 100755 core/cpu/CMakeLists.txt create mode 100755 core/cpu/cpu.cc create mode 100755 core/cpu/cpu_ut/CMakeLists.txt create mode 100755 core/cpu/cpu_ut/cpu_ut.cc create mode 100644 core/debug_is/CMakeLists.txt create mode 100755 core/debug_is/debug.cc create mode 100644 core/debug_is/debug_ex.h create mode 100644 core/debug_is/debug_is_ut/CMakeLists.txt create mode 100755 core/debug_is/debug_is_ut/debug_ut.cc create mode 100644 core/debug_is/debug_streams.cc create mode 100644 core/encryptor/CMakeLists.txt create mode 100755 core/encryptor/cpnano_base64/CMakeLists.txt create mode 100755 core/encryptor/cpnano_base64/base64.cc create mode 100755 core/encryptor/cpnano_base64/base64.h create mode 100755 core/encryptor/cpnano_base64/cpnano_base64.cc create mode 100644 core/encryptor/encryptor.cc create mode 100755 core/encryptor/encryptor_ut/CMakeLists.txt create mode 100644 core/encryptor/encryptor_ut/encryptor_ut.cc create mode 100644 core/environment/CMakeLists.txt create mode 100644 core/environment/base_evaluators.cc create mode 100644 core/environment/context.cc create mode 100644 core/environment/environment.cc create mode 100755 core/environment/environment_ut/CMakeLists.txt create mode 100755 core/environment/environment_ut/base_evaluators_ut.cc create mode 100755 core/environment/environment_ut/context_ut.cc create mode 100644 core/environment/environment_ut/environment_rest_ut.cc create mode 100644 core/environment/environment_ut/environment_ut.cc create mode 100755 core/environment/environment_ut/parsing_ut.cc create mode 100755 core/environment/environment_ut/span_ut.cc create mode 100755 core/environment/environment_ut/trace_ut.cc create mode 100755 core/environment/environment_ut/tracing_ut.cc create mode 100644 core/environment/evaluator_registration.h create mode 100644 core/environment/param_attr.cc create mode 100644 core/environment/parsing.cc create mode 100755 core/environment/span.cc create mode 100755 core/environment/trace.cc create mode 100644 core/event_is/CMakeLists.txt create mode 100644 core/event_is/event_ut/CMakeLists.txt create mode 100644 core/event_is/event_ut/event_ut.cc create mode 100644 core/event_is/listener.cc create mode 100644 core/include/attachments/attachment_types.h create mode 100755 core/include/attachments/compression_utils.h create mode 100644 core/include/attachments/http_configuration.h create mode 100755 core/include/attachments/nginx_attachment_common.h create mode 100644 core/include/attachments/nginx_attachment_util.h create mode 100755 core/include/attachments/shmem_ipc.h create mode 100755 core/include/general/buffer.h create mode 100755 core/include/general/buffer/char_iterator.h create mode 100755 core/include/general/buffer/data_container.h create mode 100755 core/include/general/buffer/helper_functions.h create mode 100755 core/include/general/buffer/internal_ptr.h create mode 100755 core/include/general/buffer/segment.h create mode 100644 core/include/general/c_common/ip_common.h create mode 100755 core/include/general/c_common/network_defs.h create mode 100755 core/include/general/c_common/networking_headers.h create mode 100644 core/include/general/common.h create mode 100644 core/include/general/config_component.h create mode 100755 core/include/general/cptest.h create mode 100644 core/include/general/cptest/cptest_basic.h create mode 100644 core/include/general/cptest/cptest_file.h create mode 100644 core/include/general/cptest/cptest_maybe.h create mode 100644 core/include/general/cptest/cptest_singleton.h create mode 100755 core/include/general/cptest/cptest_tcppacket.h create mode 100755 core/include/general/debug.h create mode 100644 core/include/general/encryptor.h create mode 100644 core/include/general/environment.h create mode 100644 core/include/general/hash_combine.h create mode 100644 core/include/general/impl/singleton.h create mode 100755 core/include/general/intelligence_comp_v2.h create mode 100644 core/include/general/logging_comp.h create mode 100644 core/include/general/mainloop.h create mode 100644 core/include/general/maybe_res.h create mode 100644 core/include/general/rest_server.h create mode 100644 core/include/general/scope_exit.h create mode 100644 core/include/general/shmpktqueue.h create mode 100644 core/include/general/singleton.h create mode 100644 core/include/general/table.h create mode 100644 core/include/general/table/entry_impl.h create mode 100644 core/include/general/table/expiration_impl.h create mode 100644 core/include/general/table/table_helpers.h create mode 100644 core/include/general/table/table_impl.h create mode 100644 core/include/general/table/table_list.h create mode 100644 core/include/general/table/table_list_iter.h create mode 100644 core/include/general/table/table_list_node.h create mode 100644 core/include/general/tenant_manager.h create mode 100755 core/include/general/time_proxy.h create mode 100644 core/include/general/tostring.h create mode 100644 core/include/internal/agent_details_reporter.h create mode 100755 core/include/internal/http_encoder.h create mode 100644 core/include/internal/instance_awareness.h create mode 100755 core/include/internal/ioctl_is.h create mode 100755 core/include/internal/mainloop/mainloop_metric.h create mode 100644 core/include/internal/messaging_buffer.h create mode 100644 core/include/internal/messaging_buffer/bucket_manager.h create mode 100644 core/include/internal/messaging_buffer/event_queue.h create mode 100644 core/include/internal/messaging_buffer/http_request_event.h create mode 100644 core/include/internal/proto_message_comp.h create mode 100755 core/include/internal/shell_cmd.h create mode 100755 core/include/internal/trap_handler.h create mode 100755 core/include/services_sdk/interfaces/i_agent_details.h create mode 100644 core/include/services_sdk/interfaces/i_agent_details_reporter.h create mode 100755 core/include/services_sdk/interfaces/i_cpu.h create mode 100644 core/include/services_sdk/interfaces/i_encryptor.h create mode 100644 core/include/services_sdk/interfaces/i_environment.h create mode 100755 core/include/services_sdk/interfaces/i_failopen.h create mode 100755 core/include/services_sdk/interfaces/i_health_check_manager.h create mode 100644 core/include/services_sdk/interfaces/i_instance_awareness.h create mode 100755 core/include/services_sdk/interfaces/i_intelligence_is_v2.h create mode 100755 core/include/services_sdk/interfaces/i_ioctl.h create mode 100644 core/include/services_sdk/interfaces/i_logging.h create mode 100644 core/include/services_sdk/interfaces/i_mainloop.h create mode 100755 core/include/services_sdk/interfaces/i_messaging.h create mode 100644 core/include/services_sdk/interfaces/i_messaging_buffer.h create mode 100755 core/include/services_sdk/interfaces/i_messaging_downloader.h create mode 100644 core/include/services_sdk/interfaces/i_rest_api.h create mode 100755 core/include/services_sdk/interfaces/i_shell_cmd.h create mode 100755 core/include/services_sdk/interfaces/i_signal_handler.h create mode 100755 core/include/services_sdk/interfaces/i_socket_is.h create mode 100644 core/include/services_sdk/interfaces/i_table.h create mode 100644 core/include/services_sdk/interfaces/i_table_iter.h create mode 100644 core/include/services_sdk/interfaces/i_tenant_manager.h create mode 100644 core/include/services_sdk/interfaces/i_time_get.h create mode 100644 core/include/services_sdk/interfaces/i_time_set.h create mode 100755 core/include/services_sdk/interfaces/i_trap_handler.h create mode 100755 core/include/services_sdk/interfaces/intelligence_is_v2/asset_source_v2.h create mode 100755 core/include/services_sdk/interfaces/intelligence_is_v2/asset_source_v2_impl.h create mode 100755 core/include/services_sdk/interfaces/intelligence_is_v2/intelligence_query_v2.h create mode 100755 core/include/services_sdk/interfaces/intelligence_is_v2/intelligence_query_v2_impl.h create mode 100755 core/include/services_sdk/interfaces/intelligence_is_v2/intelligence_types_v2.h create mode 100755 core/include/services_sdk/interfaces/intelligence_is_v2/query_filter_v2.h create mode 100755 core/include/services_sdk/interfaces/intelligence_is_v2/query_request_v2.h create mode 100755 core/include/services_sdk/interfaces/intelligence_is_v2/query_response_v2.h create mode 100755 core/include/services_sdk/interfaces/intelligence_is_v2/query_response_v2_impl.h create mode 100644 core/include/services_sdk/interfaces/intelligence_is_v2/query_types_v2.h create mode 100755 core/include/services_sdk/interfaces/intelligence_is_v2/requested_attributes_v2.h create mode 100644 core/include/services_sdk/interfaces/messaging/http_core.h create mode 100755 core/include/services_sdk/interfaces/mock/mock_agent_details.h create mode 100644 core/include/services_sdk/interfaces/mock/mock_agent_details_reporter.h create mode 100755 core/include/services_sdk/interfaces/mock/mock_cpu.h create mode 100644 core/include/services_sdk/interfaces/mock/mock_encryptor.h create mode 100644 core/include/services_sdk/interfaces/mock/mock_environment.h create mode 100644 core/include/services_sdk/interfaces/mock/mock_instance_awareness.h create mode 100755 core/include/services_sdk/interfaces/mock/mock_logging.h create mode 100644 core/include/services_sdk/interfaces/mock/mock_mainloop.h create mode 100755 core/include/services_sdk/interfaces/mock/mock_messaging.h create mode 100644 core/include/services_sdk/interfaces/mock/mock_rest_api.h create mode 100755 core/include/services_sdk/interfaces/mock/mock_shell_cmd.h create mode 100755 core/include/services_sdk/interfaces/mock/mock_socket_is.h create mode 100755 core/include/services_sdk/interfaces/mock/mock_table.h create mode 100644 core/include/services_sdk/interfaces/mock/mock_tenant_manager.h create mode 100755 core/include/services_sdk/interfaces/mock/mock_time_get.h create mode 100755 core/include/services_sdk/resources/agent_details.h create mode 100644 core/include/services_sdk/resources/agent_details_report.h create mode 100755 core/include/services_sdk/resources/component.h create mode 100755 core/include/services_sdk/resources/component_is/components_list_impl.h create mode 100755 core/include/services_sdk/resources/component_is/node_components_impl.h create mode 100755 core/include/services_sdk/resources/components_list.h create mode 100644 core/include/services_sdk/resources/config.h create mode 100644 core/include/services_sdk/resources/config/config_exception.h create mode 100644 core/include/services_sdk/resources/config/config_impl.h create mode 100644 core/include/services_sdk/resources/config/config_loader.h create mode 100644 core/include/services_sdk/resources/config/config_types.h create mode 100644 core/include/services_sdk/resources/config/generic_config.h create mode 100644 core/include/services_sdk/resources/config/i_config.h create mode 100755 core/include/services_sdk/resources/config/i_config_iterator.h create mode 100644 core/include/services_sdk/resources/config/range_config.h create mode 100644 core/include/services_sdk/resources/config/type_wrapper.h create mode 100644 core/include/services_sdk/resources/context.h create mode 100755 core/include/services_sdk/resources/cpu.h create mode 100755 core/include/services_sdk/resources/cpu/cpu_metric.h create mode 100755 core/include/services_sdk/resources/cpu/failopen_mode_status.h create mode 100755 core/include/services_sdk/resources/debug_flags.h create mode 100644 core/include/services_sdk/resources/environment/base_evaluators.h create mode 100644 core/include/services_sdk/resources/environment/context_impl.h create mode 100644 core/include/services_sdk/resources/environment/evaluator_templates.h create mode 100644 core/include/services_sdk/resources/environment/evaluators_repo.h create mode 100644 core/include/services_sdk/resources/environment/param.h create mode 100644 core/include/services_sdk/resources/environment/parsing_functions.h create mode 100755 core/include/services_sdk/resources/environment/span.h create mode 100755 core/include/services_sdk/resources/environment/trace.h create mode 100755 core/include/services_sdk/resources/environment/tracing_metric.h create mode 100644 core/include/services_sdk/resources/environment_evaluator.h create mode 100644 core/include/services_sdk/resources/event.h create mode 100644 core/include/services_sdk/resources/event_is/event_impl.h create mode 100644 core/include/services_sdk/resources/event_is/listener_impl.h create mode 100755 core/include/services_sdk/resources/generic_metric.h create mode 100755 core/include/services_sdk/resources/health_check_status/health_check_status.h create mode 100755 core/include/services_sdk/resources/intelligence_filter.h create mode 100644 core/include/services_sdk/resources/intelligence_is/data_map.h create mode 100755 core/include/services_sdk/resources/intelligence_is/data_string.h create mode 100644 core/include/services_sdk/resources/intelligence_is/data_vector.h create mode 100755 core/include/services_sdk/resources/intelligence_is/read_attribute_impl.h create mode 100644 core/include/services_sdk/resources/intelligence_is_v2/data_map_v2.h create mode 100755 core/include/services_sdk/resources/intelligence_is_v2/data_string_v2.h create mode 100755 core/include/services_sdk/resources/intelligence_is_v2/data_vector_v2.h create mode 100755 core/include/services_sdk/resources/intelligence_is_v2/read_attribute_v2_impl.h create mode 100644 core/include/services_sdk/resources/listener.h create mode 100755 core/include/services_sdk/resources/log_generator.h create mode 100755 core/include/services_sdk/resources/memory_consumption.h create mode 100755 core/include/services_sdk/resources/metric/all_metric_event.h create mode 100755 core/include/services_sdk/resources/metric/average.h create mode 100755 core/include/services_sdk/resources/metric/counter.h create mode 100755 core/include/services_sdk/resources/metric/last_reported_value.h create mode 100755 core/include/services_sdk/resources/metric/max.h create mode 100755 core/include/services_sdk/resources/metric/metric_calc.h create mode 100644 core/include/services_sdk/resources/metric/metric_map.h create mode 100755 core/include/services_sdk/resources/metric/min.h create mode 100755 core/include/services_sdk/resources/metric/top_values.h create mode 100755 core/include/services_sdk/resources/read_attribute.h create mode 100755 core/include/services_sdk/resources/read_attribute_v2.h create mode 100755 core/include/services_sdk/resources/report/base_field.h create mode 100755 core/include/services_sdk/resources/report/log_rest.h create mode 100755 core/include/services_sdk/resources/report/report.h create mode 100755 core/include/services_sdk/resources/report/report_bulks.h create mode 100755 core/include/services_sdk/resources/report/report_enums.h create mode 100644 core/include/services_sdk/resources/rest.h create mode 100644 core/include/services_sdk/resources/table/i_table_impl.h create mode 100644 core/include/services_sdk/resources/table/opaque_basic.h create mode 100644 core/include/services_sdk/resources/table/opaque_reg.h create mode 100644 core/include/services_sdk/resources/table/opaque_repo.h create mode 100644 core/include/services_sdk/resources/table_iter.h create mode 100644 core/include/services_sdk/resources/table_opaque.h create mode 100644 core/include/services_sdk/resources/tag_and_enum_management.h create mode 100644 core/include/services_sdk/resources/version.h create mode 100755 core/include/services_sdk/utilities/agent_core_utilities.h create mode 100644 core/include/services_sdk/utilities/cache.h create mode 100644 core/include/services_sdk/utilities/caching/cache_impl.h create mode 100644 core/include/services_sdk/utilities/caching/cache_types.h create mode 100755 core/include/services_sdk/utilities/connkey.h create mode 100755 core/include/services_sdk/utilities/customized_cereal_map.h create mode 100755 core/include/services_sdk/utilities/customized_cereal_multimap.h create mode 100644 core/include/services_sdk/utilities/enum_array.h create mode 100644 core/include/services_sdk/utilities/enum_range.h create mode 100644 core/include/services_sdk/utilities/flags.h create mode 100644 core/include/services_sdk/utilities/fog_rest_error.h create mode 100644 core/include/services_sdk/utilities/rest/rest_helper.h create mode 100644 core/include/services_sdk/utilities/rest/rest_param.h create mode 100644 core/include/services_sdk/utilities/rest/schema_printer.h create mode 100755 core/include/services_sdk/utilities/sasal.h create mode 100755 core/include/services_sdk/utilities/socket_is.h create mode 100644 core/include/services_sdk/utilities/time_print.h create mode 100644 core/include/services_sdk/utilities/virtual_container.h create mode 100644 core/include/services_sdk/utilities/virtual_modifiers.h create mode 100644 core/instance_awareness/CMakeLists.txt create mode 100644 core/instance_awareness/instance_awareness.cc create mode 100644 core/instance_awareness/instance_awareness_ut/CMakeLists.txt create mode 100644 core/instance_awareness/instance_awareness_ut/instance_awareness_ut.cc create mode 100755 core/intelligence_is_v2/CMakeLists.txt create mode 100755 core/intelligence_is_v2/intelligence_comp_v2.cc create mode 100755 core/intelligence_is_v2/intelligence_is_v2_ut/CMakeLists.txt create mode 100755 core/intelligence_is_v2/intelligence_is_v2_ut/intelligence_comp_v2_ut.cc create mode 100755 core/intelligence_is_v2/intelligence_is_v2_ut/offline_intelligence_files_v2/1.2.3.4 create mode 100755 core/intelligence_is_v2/intelligence_is_v2_ut/query_request_v2_ut.cc create mode 100755 core/intelligence_is_v2/intelligence_is_v2_ut/query_response_v2_ut.cc create mode 100755 core/intelligence_is_v2/intelligence_types_v2.cc create mode 100755 core/intelligence_is_v2/query_filter_v2.cc create mode 100755 core/intelligence_is_v2/query_request_v2.cc create mode 100644 core/intelligence_is_v2/query_types_v2.cc create mode 100755 core/intelligence_is_v2/requested_attributes_v2.cc create mode 100644 core/logging/CMakeLists.txt create mode 100755 core/logging/cef_stream.cc create mode 100755 core/logging/debug_stream.cc create mode 100755 core/logging/file_stream.cc create mode 100755 core/logging/fog_stream.cc create mode 100755 core/logging/log_generator.cc create mode 100755 core/logging/log_streams.h create mode 100755 core/logging/logging.cc create mode 100755 core/logging/logging_metric.h create mode 100755 core/logging/logging_ut/CMakeLists.txt create mode 100755 core/logging/logging_ut/logging_ut.cc create mode 100755 core/logging/syslog_stream.cc create mode 100644 core/mainloop/CMakeLists.txt create mode 100644 core/mainloop/coroutine.cc create mode 100644 core/mainloop/coroutine.h create mode 100644 core/mainloop/mainloop.cc create mode 100644 core/mainloop/mainloop_ut/CMakeLists.txt create mode 100644 core/mainloop/mainloop_ut/mainloop_ut.cc create mode 100755 core/memory_consumption/CMakeLists.txt create mode 100755 core/memory_consumption/memory_consumption.cc create mode 100755 core/memory_consumption/memory_consumption_ut/CMakeLists.txt create mode 100755 core/memory_consumption/memory_consumption_ut/memory_consumption_ut.cc create mode 100755 core/memory_consumption/memory_metric.h create mode 100644 core/message/CMakeLists.txt create mode 100644 core/message/http_core.cc create mode 100755 core/message/http_decoder.cc create mode 100755 core/message/http_decoder.h create mode 100755 core/message/http_encoder.cc create mode 100644 core/message/i_message_decoder.h create mode 100755 core/message/message.cc create mode 100755 core/message/message_metric.h create mode 100644 core/message/smart_bio.h create mode 100644 core/messaging_buffer/CMakeLists.txt create mode 100644 core/messaging_buffer/bucket_manager.cc create mode 100644 core/messaging_buffer/event_queue.cc create mode 100644 core/messaging_buffer/messaging_buffer.cc create mode 100644 core/messaging_buffer/messaging_buffer_ut/CMakeLists.txt create mode 100644 core/messaging_buffer/messaging_buffer_ut/messaging_buffer_ut.cc create mode 100755 core/metric/CMakeLists.txt create mode 100755 core/metric/generic_metric.cc create mode 100755 core/metric/metric_ut/CMakeLists.txt create mode 100755 core/metric/metric_ut/metric_ut.cc create mode 100644 core/report/CMakeLists.txt create mode 100644 core/report/report.cc create mode 100644 core/report/report_ut/CMakeLists.txt create mode 100644 core/report/report_ut/report_ut.cc create mode 100755 core/report/tag_and_enum_management.cc create mode 100644 core/rest/CMakeLists.txt create mode 100644 core/rest/i_rest_invoke.h create mode 100644 core/rest/rest.cc create mode 100644 core/rest/rest_conn.cc create mode 100644 core/rest/rest_conn.h create mode 100644 core/rest/rest_server.cc create mode 100755 core/rest/rest_ut/CMakeLists.txt create mode 100755 core/rest/rest_ut/rest_config_ut.cc create mode 100755 core/rest/rest_ut/rest_must_param_ut.cc create mode 100644 core/rest/rest_ut/rest_schema_ut.cc create mode 100755 core/shell_cmd/CMakeLists.txt create mode 100755 core/shell_cmd/shell_cmd.cc create mode 100644 core/shm_pkt_queue/CMakeLists.txt create mode 100644 core/shm_pkt_queue/shm_pkt_queue.cc create mode 100644 core/shm_pkt_queue/shm_pkt_queue_ut/CMakeLists.txt create mode 100644 core/shm_pkt_queue/shm_pkt_queue_ut/shm_pkt_queue_ut.cc create mode 100755 core/shmem_ipc/CMakeLists.txt create mode 100755 core/shmem_ipc/shared_ipc_debug.h create mode 100755 core/shmem_ipc/shared_ring_queue.c create mode 100755 core/shmem_ipc/shared_ring_queue.h create mode 100755 core/shmem_ipc/shmem_ipc.c create mode 100755 core/shmem_ipc/shmem_ipc_ut/CMakeLists.txt create mode 100755 core/shmem_ipc/shmem_ipc_ut/shared_ring_queue_ut.cc create mode 100755 core/shmem_ipc/shmem_ipc_ut/shmem_ipc_ut.cc create mode 100644 core/singleton/CMakeLists.txt create mode 100644 core/singleton/singleton.cc create mode 100644 core/singleton/singleton_ut/CMakeLists.txt create mode 100644 core/singleton/singleton_ut/singleton_ut.cc create mode 100755 core/socket_is/CMakeLists.txt create mode 100755 core/socket_is/socket_is.cc create mode 100644 core/table/CMakeLists.txt create mode 100644 core/table/opaque_repo.cc create mode 100644 core/table/table.cc create mode 100644 core/tenant_manager/CMakeLists.txt create mode 100644 core/tenant_manager/tenant_manager.cc create mode 100644 core/time_proxy/CMakeLists.txt create mode 100644 core/time_proxy/time_proxy.cc create mode 100644 core/time_proxy/time_proxy_ut/CMakeLists.txt create mode 100755 core/time_proxy/time_proxy_ut/time_print_ut.cc create mode 100644 core/time_proxy/time_proxy_ut/time_proxy_ut.cc create mode 100644 core/version/CMakeLists.txt create mode 100755 core/version/build_version_vars_h.py create mode 100755 core/version/version.cc create mode 100644 core/version/version_ut/CMakeLists.txt create mode 100755 core/version/version_ut/version_ut.cc create mode 100644 cppcheck.cmake create mode 100644 external/C-Mock/.travis.yml create mode 100644 external/C-Mock/LICENSE.md create mode 100644 external/C-Mock/README.md create mode 100755 external/C-Mock/bin/cmock-config create mode 100644 external/C-Mock/include/cmock/cmock-function-class-mockers.h create mode 100644 external/C-Mock/include/cmock/cmock-function-mockers.h create mode 100644 external/C-Mock/include/cmock/cmock-internal.h create mode 100644 external/C-Mock/include/cmock/cmock-spec-builders.h create mode 100644 external/C-Mock/include/cmock/cmock.h create mode 100644 external/C-Mock/test/cmock-function-class-mockers-old-style_test.cc create mode 100644 external/C-Mock/test/cmock-function-class-mockers_test.cc create mode 100644 external/C-Mock/test/cmock-function-mockers_test.cc create mode 100644 external/C-Mock/test/cmock-spec-builders_test.cc create mode 100644 external/C-Mock/test/math.c create mode 100644 external/C-Mock/test/math.h create mode 100644 external/CMakeLists.txt create mode 100644 external/cereal/LICENSE create mode 100644 external/cereal/access.hpp create mode 100644 external/cereal/archives/adapters.hpp create mode 100644 external/cereal/archives/binary.hpp create mode 100644 external/cereal/archives/json.hpp create mode 100644 external/cereal/archives/portable_binary.hpp create mode 100644 external/cereal/archives/xml.hpp create mode 100644 external/cereal/cereal.hpp create mode 100644 external/cereal/details/helpers.hpp create mode 100644 external/cereal/details/polymorphic_impl.hpp create mode 100644 external/cereal/details/polymorphic_impl_fwd.hpp create mode 100644 external/cereal/details/static_object.hpp create mode 100644 external/cereal/details/traits.hpp create mode 100644 external/cereal/details/util.hpp create mode 100644 external/cereal/external/base64.hpp create mode 100644 external/cereal/external/rapidjson/allocators.h create mode 100644 external/cereal/external/rapidjson/cursorstreamwrapper.h create mode 100644 external/cereal/external/rapidjson/document.h create mode 100644 external/cereal/external/rapidjson/encodedstream.h create mode 100644 external/cereal/external/rapidjson/encodings.h create mode 100644 external/cereal/external/rapidjson/error/en.h create mode 100644 external/cereal/external/rapidjson/error/error.h create mode 100644 external/cereal/external/rapidjson/filereadstream.h create mode 100644 external/cereal/external/rapidjson/filewritestream.h create mode 100644 external/cereal/external/rapidjson/fwd.h create mode 100644 external/cereal/external/rapidjson/internal/biginteger.h create mode 100644 external/cereal/external/rapidjson/internal/clzll.h create mode 100644 external/cereal/external/rapidjson/internal/diyfp.h create mode 100644 external/cereal/external/rapidjson/internal/dtoa.h create mode 100644 external/cereal/external/rapidjson/internal/ieee754.h create mode 100644 external/cereal/external/rapidjson/internal/itoa.h create mode 100644 external/cereal/external/rapidjson/internal/meta.h create mode 100644 external/cereal/external/rapidjson/internal/pow10.h create mode 100644 external/cereal/external/rapidjson/internal/regex.h create mode 100644 external/cereal/external/rapidjson/internal/stack.h create mode 100644 external/cereal/external/rapidjson/internal/strfunc.h create mode 100644 external/cereal/external/rapidjson/internal/strtod.h create mode 100644 external/cereal/external/rapidjson/internal/swap.h create mode 100644 external/cereal/external/rapidjson/istreamwrapper.h create mode 100644 external/cereal/external/rapidjson/memorybuffer.h create mode 100644 external/cereal/external/rapidjson/memorystream.h create mode 100644 external/cereal/external/rapidjson/msinttypes/inttypes.h create mode 100644 external/cereal/external/rapidjson/msinttypes/stdint.h create mode 100644 external/cereal/external/rapidjson/ostreamwrapper.h create mode 100644 external/cereal/external/rapidjson/pointer.h create mode 100644 external/cereal/external/rapidjson/prettywriter.h create mode 100644 external/cereal/external/rapidjson/rapidjson.h create mode 100644 external/cereal/external/rapidjson/reader.h create mode 100644 external/cereal/external/rapidjson/schema.h create mode 100644 external/cereal/external/rapidjson/stream.h create mode 100644 external/cereal/external/rapidjson/stringbuffer.h create mode 100644 external/cereal/external/rapidjson/uri.h create mode 100644 external/cereal/external/rapidjson/writer.h create mode 100644 external/cereal/external/rapidxml/license.txt create mode 100644 external/cereal/external/rapidxml/manual.html create mode 100644 external/cereal/external/rapidxml/rapidxml.hpp create mode 100644 external/cereal/external/rapidxml/rapidxml_iterators.hpp create mode 100644 external/cereal/external/rapidxml/rapidxml_print.hpp create mode 100644 external/cereal/external/rapidxml/rapidxml_utils.hpp create mode 100644 external/cereal/macros.hpp create mode 100644 external/cereal/types/array.hpp create mode 100644 external/cereal/types/base_class.hpp create mode 100644 external/cereal/types/bitset.hpp create mode 100644 external/cereal/types/boost_variant.hpp create mode 100644 external/cereal/types/chrono.hpp create mode 100644 external/cereal/types/common.hpp create mode 100644 external/cereal/types/complex.hpp create mode 100644 external/cereal/types/concepts/pair_associative_container.hpp create mode 100644 external/cereal/types/deque.hpp create mode 100644 external/cereal/types/forward_list.hpp create mode 100644 external/cereal/types/functional.hpp create mode 100644 external/cereal/types/list.hpp create mode 100644 external/cereal/types/map.hpp create mode 100644 external/cereal/types/memory.hpp create mode 100644 external/cereal/types/polymorphic.hpp create mode 100644 external/cereal/types/queue.hpp create mode 100644 external/cereal/types/set.hpp create mode 100644 external/cereal/types/stack.hpp create mode 100644 external/cereal/types/string.hpp create mode 100644 external/cereal/types/tuple.hpp create mode 100644 external/cereal/types/unordered_map.hpp create mode 100644 external/cereal/types/unordered_set.hpp create mode 100644 external/cereal/types/utility.hpp create mode 100644 external/cereal/types/valarray.hpp create mode 100644 external/cereal/types/vector.hpp create mode 100755 external/makeself/COPYING create mode 100755 external/makeself/README.md create mode 100755 external/makeself/makeself-header.sh create mode 100755 external/makeself/makeself.1 create mode 100755 external/makeself/makeself.lsm create mode 100755 external/makeself/makeself.sh create mode 100755 external/makeself/test/datetest create mode 100644 external/picojson/.clang-format create mode 100644 external/picojson/.travis.yml create mode 100644 external/picojson/Changes create mode 100644 external/picojson/LICENSE create mode 100644 external/picojson/README.mkdn create mode 100644 external/picojson/examples/github-issues.cc create mode 100644 external/picojson/examples/iostream.cc create mode 100644 external/picojson/examples/streaming.cc create mode 100644 external/picojson/picojson.h create mode 100644 external/picojson/picotest/picotest.c create mode 100644 external/picojson/picotest/picotest.h create mode 100644 external/picojson/test.cc create mode 100644 external/yajl/BUILDING create mode 100644 external/yajl/BUILDING.win32 create mode 100644 external/yajl/CMakeLists.txt create mode 100644 external/yajl/COPYING create mode 100644 external/yajl/ChangeLog create mode 100644 external/yajl/README create mode 100644 external/yajl/TODO create mode 100644 external/yajl/YAJLDoc.cmake create mode 100755 external/yajl/configure create mode 100644 external/yajl/example/CMakeLists.txt create mode 100644 external/yajl/example/README.md create mode 100755 external/yajl/example/parse_config create mode 100644 external/yajl/example/parse_config.c create mode 100644 external/yajl/example/sample.config create mode 100644 external/yajl/perf/CMakeLists.txt create mode 100644 external/yajl/perf/documents.c create mode 100644 external/yajl/perf/documents.h create mode 100755 external/yajl/perf/perftest create mode 100644 external/yajl/perf/perftest.c create mode 100644 external/yajl/reformatter/CMakeLists.txt create mode 100644 external/yajl/reformatter/json_reformat.c create mode 100644 external/yajl/src/CMakeLists.txt create mode 100644 external/yajl/src/YAJL.dxy create mode 100644 external/yajl/src/api/yajl_common.h create mode 100644 external/yajl/src/api/yajl_gen.h create mode 100644 external/yajl/src/api/yajl_parse.h create mode 100644 external/yajl/src/api/yajl_tree.h create mode 100644 external/yajl/src/api/yajl_version.h.cmake create mode 100644 external/yajl/src/yajl create mode 100644 external/yajl/src/yajl.c create mode 100644 external/yajl/src/yajl.pc.cmake create mode 100644 external/yajl/src/yajl_alloc.c create mode 100644 external/yajl/src/yajl_alloc.h create mode 100644 external/yajl/src/yajl_buf.c create mode 100644 external/yajl/src/yajl_buf.h create mode 100644 external/yajl/src/yajl_bytestack.h create mode 100644 external/yajl/src/yajl_encode.c create mode 100644 external/yajl/src/yajl_encode.h create mode 100644 external/yajl/src/yajl_gen.c create mode 100644 external/yajl/src/yajl_lex.c create mode 100644 external/yajl/src/yajl_lex.h create mode 100644 external/yajl/src/yajl_parser.c create mode 100644 external/yajl/src/yajl_parser.h create mode 100644 external/yajl/src/yajl_tree.c create mode 100644 external/yajl/src/yajl_version.c create mode 100644 external/yajl/test/CMakeLists.txt create mode 100644 external/yajl/test/api/CMakeLists.txt create mode 100755 external/yajl/test/api/gen-extra-close create mode 100644 external/yajl/test/api/gen-extra-close.c create mode 100755 external/yajl/test/api/run_tests.sh create mode 100644 external/yajl/test/parsing/CMakeLists.txt create mode 100644 external/yajl/test/parsing/cases/ac_difficult_json_c_test_case_with_comments.json create mode 100644 external/yajl/test/parsing/cases/ac_difficult_json_c_test_case_with_comments.json.gold create mode 100644 external/yajl/test/parsing/cases/ac_simple_with_comments.json create mode 100644 external/yajl/test/parsing/cases/ac_simple_with_comments.json.gold create mode 100644 external/yajl/test/parsing/cases/ag_false_then_garbage.json create mode 100644 external/yajl/test/parsing/cases/ag_false_then_garbage.json.gold create mode 100644 external/yajl/test/parsing/cases/ag_null_then_garbage.json create mode 100644 external/yajl/test/parsing/cases/ag_null_then_garbage.json.gold create mode 100644 external/yajl/test/parsing/cases/ag_true_then_garbage.json create mode 100644 external/yajl/test/parsing/cases/ag_true_then_garbage.json.gold create mode 100644 external/yajl/test/parsing/cases/am_eof.json create mode 100644 external/yajl/test/parsing/cases/am_eof.json.gold create mode 100644 external/yajl/test/parsing/cases/am_integers.json create mode 100644 external/yajl/test/parsing/cases/am_integers.json.gold create mode 100644 external/yajl/test/parsing/cases/am_multiple.json create mode 100644 external/yajl/test/parsing/cases/am_multiple.json.gold create mode 100644 external/yajl/test/parsing/cases/am_stuff.json create mode 100644 external/yajl/test/parsing/cases/am_stuff.json.gold create mode 100644 external/yajl/test/parsing/cases/ap_array_open.json create mode 100644 external/yajl/test/parsing/cases/ap_array_open.json.gold create mode 100644 external/yajl/test/parsing/cases/ap_eof_str.json create mode 100644 external/yajl/test/parsing/cases/ap_eof_str.json.gold create mode 100644 external/yajl/test/parsing/cases/ap_map_open.json create mode 100644 external/yajl/test/parsing/cases/ap_map_open.json.gold create mode 100644 external/yajl/test/parsing/cases/ap_partial_ok.json create mode 100644 external/yajl/test/parsing/cases/ap_partial_ok.json.gold create mode 100644 external/yajl/test/parsing/cases/array.json create mode 100644 external/yajl/test/parsing/cases/array.json.gold create mode 100644 external/yajl/test/parsing/cases/array_close.json create mode 100644 external/yajl/test/parsing/cases/array_close.json.gold create mode 100644 external/yajl/test/parsing/cases/bignums.json create mode 100644 external/yajl/test/parsing/cases/bignums.json.gold create mode 100644 external/yajl/test/parsing/cases/bogus_char.json create mode 100644 external/yajl/test/parsing/cases/bogus_char.json.gold create mode 100644 external/yajl/test/parsing/cases/codepoints_from_unicode_org.json create mode 100644 external/yajl/test/parsing/cases/codepoints_from_unicode_org.json.gold create mode 100644 external/yajl/test/parsing/cases/deep_arrays.json create mode 100644 external/yajl/test/parsing/cases/deep_arrays.json.gold create mode 100644 external/yajl/test/parsing/cases/difficult_json_c_test_case.json create mode 100644 external/yajl/test/parsing/cases/difficult_json_c_test_case.json.gold create mode 100644 external/yajl/test/parsing/cases/doubles.json create mode 100644 external/yajl/test/parsing/cases/doubles.json.gold create mode 100644 external/yajl/test/parsing/cases/doubles_in_array.json create mode 100644 external/yajl/test/parsing/cases/doubles_in_array.json.gold create mode 100644 external/yajl/test/parsing/cases/empty_array.json create mode 100644 external/yajl/test/parsing/cases/empty_array.json.gold create mode 100644 external/yajl/test/parsing/cases/empty_string.json create mode 100644 external/yajl/test/parsing/cases/empty_string.json.gold create mode 100644 external/yajl/test/parsing/cases/escaped_bulgarian.json create mode 100644 external/yajl/test/parsing/cases/escaped_bulgarian.json.gold create mode 100644 external/yajl/test/parsing/cases/escaped_foobar.json create mode 100644 external/yajl/test/parsing/cases/escaped_foobar.json.gold create mode 100644 external/yajl/test/parsing/cases/false.json create mode 100644 external/yajl/test/parsing/cases/false.json.gold create mode 100644 external/yajl/test/parsing/cases/fg_false_then_garbage.json create mode 100644 external/yajl/test/parsing/cases/fg_false_then_garbage.json.gold create mode 100644 external/yajl/test/parsing/cases/fg_issue_7.json create mode 100644 external/yajl/test/parsing/cases/fg_issue_7.json.gold create mode 100644 external/yajl/test/parsing/cases/fg_null_then_garbage.json create mode 100644 external/yajl/test/parsing/cases/fg_null_then_garbage.json.gold create mode 100644 external/yajl/test/parsing/cases/fg_true_then_garbage.json create mode 100644 external/yajl/test/parsing/cases/fg_true_then_garbage.json.gold create mode 100644 external/yajl/test/parsing/cases/four_byte_utf8.json create mode 100644 external/yajl/test/parsing/cases/four_byte_utf8.json.gold create mode 100644 external/yajl/test/parsing/cases/high_overflow.json create mode 100644 external/yajl/test/parsing/cases/high_overflow.json.gold create mode 100644 external/yajl/test/parsing/cases/integers.json create mode 100644 external/yajl/test/parsing/cases/integers.json.gold create mode 100644 external/yajl/test/parsing/cases/invalid_utf8.json create mode 100644 external/yajl/test/parsing/cases/invalid_utf8.json.gold create mode 100644 external/yajl/test/parsing/cases/isolated_surrogate_marker.json create mode 100644 external/yajl/test/parsing/cases/isolated_surrogate_marker.json.gold create mode 100644 external/yajl/test/parsing/cases/leading_zero_in_number.json create mode 100644 external/yajl/test/parsing/cases/leading_zero_in_number.json.gold create mode 100644 external/yajl/test/parsing/cases/lonely_minus_sign.json create mode 100644 external/yajl/test/parsing/cases/lonely_minus_sign.json.gold create mode 100644 external/yajl/test/parsing/cases/lonely_number.json create mode 100644 external/yajl/test/parsing/cases/lonely_number.json.gold create mode 100644 external/yajl/test/parsing/cases/low_overflow.json create mode 100644 external/yajl/test/parsing/cases/low_overflow.json.gold create mode 100644 external/yajl/test/parsing/cases/map_close.json create mode 100644 external/yajl/test/parsing/cases/map_close.json.gold create mode 100644 external/yajl/test/parsing/cases/missing_integer_after_decimal_point.json create mode 100644 external/yajl/test/parsing/cases/missing_integer_after_decimal_point.json.gold create mode 100644 external/yajl/test/parsing/cases/missing_integer_after_exponent.json create mode 100644 external/yajl/test/parsing/cases/missing_integer_after_exponent.json.gold create mode 100644 external/yajl/test/parsing/cases/multiple.json create mode 100644 external/yajl/test/parsing/cases/multiple.json.gold create mode 100644 external/yajl/test/parsing/cases/non_utf8_char_in_string.json create mode 100644 external/yajl/test/parsing/cases/non_utf8_char_in_string.json.gold create mode 100644 external/yajl/test/parsing/cases/np_partial_bad.json create mode 100644 external/yajl/test/parsing/cases/np_partial_bad.json.gold create mode 100644 external/yajl/test/parsing/cases/null.json create mode 100644 external/yajl/test/parsing/cases/null.json.gold create mode 100644 external/yajl/test/parsing/cases/nulls_and_bools.json create mode 100644 external/yajl/test/parsing/cases/nulls_and_bools.json.gold create mode 100644 external/yajl/test/parsing/cases/simple.json create mode 100644 external/yajl/test/parsing/cases/simple.json.gold create mode 100644 external/yajl/test/parsing/cases/simple_with_comments.json create mode 100644 external/yajl/test/parsing/cases/simple_with_comments.json.gold create mode 100644 external/yajl/test/parsing/cases/string_invalid_escape.json create mode 100644 external/yajl/test/parsing/cases/string_invalid_escape.json.gold create mode 100644 external/yajl/test/parsing/cases/string_invalid_hex_char.json create mode 100644 external/yajl/test/parsing/cases/string_invalid_hex_char.json.gold create mode 100644 external/yajl/test/parsing/cases/string_with_escapes.json create mode 100644 external/yajl/test/parsing/cases/string_with_escapes.json.gold create mode 100644 external/yajl/test/parsing/cases/string_with_invalid_newline.json create mode 100644 external/yajl/test/parsing/cases/string_with_invalid_newline.json.gold create mode 100644 external/yajl/test/parsing/cases/three_byte_utf8.json create mode 100644 external/yajl/test/parsing/cases/three_byte_utf8.json.gold create mode 100644 external/yajl/test/parsing/cases/true.json create mode 100644 external/yajl/test/parsing/cases/true.json.gold create mode 100644 external/yajl/test/parsing/cases/unescaped_bulgarian.json create mode 100644 external/yajl/test/parsing/cases/unescaped_bulgarian.json.gold create mode 100644 external/yajl/test/parsing/cases/zerobyte.json create mode 100644 external/yajl/test/parsing/cases/zerobyte.json.gold create mode 100755 external/yajl/test/parsing/run_tests.sh create mode 100755 external/yajl/test/parsing/yajl_test create mode 100644 external/yajl/test/parsing/yajl_test.c create mode 100644 external/yajl/verify/CMakeLists.txt create mode 100755 external/yajl/verify/json_verify create mode 100644 external/yajl/verify/json_verify.c create mode 100755 external/yajl/yajl-2.1.1/bin/json_reformat create mode 100755 external/yajl/yajl-2.1.1/bin/json_verify create mode 100644 external/yajl/yajl-2.1.1/include/yajl/yajl_common.h create mode 100644 external/yajl/yajl-2.1.1/include/yajl/yajl_gen.h create mode 100644 external/yajl/yajl-2.1.1/include/yajl/yajl_parse.h create mode 100644 external/yajl/yajl-2.1.1/include/yajl/yajl_tree.h create mode 100644 external/yajl/yajl-2.1.1/include/yajl/yajl_version.h create mode 100644 nodes/CMakeLists.txt create mode 100755 nodes/attachment_registration_manager/CMakeLists.txt create mode 100644 nodes/attachment_registration_manager/main.cc create mode 100755 nodes/attachment_registration_manager/package/CMakeLists.txt create mode 100644 nodes/attachment_registration_manager/package/cp-nano-attachment-registration-manager.cfg create mode 100755 nodes/attachment_registration_manager/package/debug-conf.json create mode 100755 nodes/attachment_registration_manager/package/install-attachment-registration-manager.sh create mode 100644 nodes/attachment_registration_manager/package/service-conf.json create mode 100755 nodes/http_transaction_handler/CMakeLists.txt create mode 100755 nodes/http_transaction_handler/main.cc create mode 100755 nodes/http_transaction_handler/package/CMakeLists.txt create mode 100755 nodes/http_transaction_handler/package/cp-nano-http-transaction-handler-conf-container.json create mode 100755 nodes/http_transaction_handler/package/cp-nano-http-transaction-handler-conf.json create mode 100755 nodes/http_transaction_handler/package/cp-nano-http-transaction-handler-debug-conf.json create mode 100755 nodes/http_transaction_handler/package/cp-nano-http-transaction-handler.cfg create mode 100755 nodes/http_transaction_handler/package/install-http-transaction-handler.sh create mode 100755 nodes/http_transaction_handler/package/k8s-log-file-handler.sh create mode 100755 nodes/orchestration/CMakeLists.txt create mode 100755 nodes/orchestration/main.cc create mode 100755 nodes/orchestration/package/CMakeLists.txt create mode 100644 nodes/orchestration/package/EULA.txt create mode 100644 nodes/orchestration/package/Licenses-for-Third-Party-Components.txt create mode 100644 nodes/orchestration/package/certificate/ngen.body.crt create mode 100755 nodes/orchestration/package/certificate/public-keys/cloud-ngen.pem create mode 100755 nodes/orchestration/package/certificate/public-keys/dev-i2.pem create mode 100755 nodes/orchestration/package/certificate/public-keys/i2.pem create mode 100755 nodes/orchestration/package/certificate/public-keys/public-key-general.pem create mode 100755 nodes/orchestration/package/certificate/public-keys/stg-i2.pem create mode 100755 nodes/orchestration/package/configuration/cp-nano-orchestration-conf.json create mode 100755 nodes/orchestration/package/configuration/cp-nano-orchestration-debug-conf.json create mode 100755 nodes/orchestration/package/configuration/orchestration.cfg create mode 100755 nodes/orchestration/package/cp-agent-info.sh create mode 100755 nodes/orchestration/package/cp-agent-uninstall.sh create mode 100755 nodes/orchestration/package/cp-nano-cli.sh create mode 100644 nodes/orchestration/package/cp-nano-package-list create mode 100755 nodes/orchestration/package/cpnano_debug/CMakeLists.txt create mode 100755 nodes/orchestration/package/cpnano_debug/cpnano_debug.cc create mode 100755 nodes/orchestration/package/cpnano_json/CMakeLists.txt create mode 100755 nodes/orchestration/package/cpnano_json/cpnano_json.cc create mode 100644 nodes/orchestration/package/k8s-check-update-listener.sh create mode 100755 nodes/orchestration/package/k8s-check-update-trigger.sh create mode 100755 nodes/orchestration/package/orchestration_package.sh create mode 100755 nodes/orchestration/package/service/arm32_openwrt/nano_agent.init create mode 100755 nodes/orchestration/package/service/smb/nano_agent.init create mode 100755 nodes/orchestration/package/service/x86/ubuntu14/nano_agent.conf create mode 100755 nodes/orchestration/package/service/x86/ubuntu14/nano_agent.init create mode 100755 nodes/orchestration/package/service/x86/ubuntu16/nano_agent.service create mode 100755 nodes/orchestration/package/watchdog/access_pre_init create mode 100755 nodes/orchestration/package/watchdog/wait-for-networking-inspection-modules.sh create mode 100755 nodes/orchestration/package/watchdog/watchdog create mode 100755 nodes/orchestration/scripts/cp-nano-makefile-generator.sh create mode 100644 nodes/packaging.cmake create mode 100644 unit_test.cmake diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..bb356b2 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,38 @@ +cmake_minimum_required (VERSION 2.8.4) +project (ngen) + +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fPIC -Wall -Wno-terminate -Dalpine") + +find_package(Boost REQUIRED) +find_package(ZLIB REQUIRED) +find_package(GTest REQUIRED) + +include(cppcheck.cmake) + +include_directories(${Boost_INCLUDE_DIRS}) +link_directories(${OPENSSL_ROOT_DIR}/lib) +include_directories(${ZLIB_INCLUDE_DIR}) +link_directories(${ZLIB_LIBRARY}) +include_directories(/usr/include/libxml2) +include_directories(/usr/src/googletest/googlemock/include) + +include(unit_test.cmake) + +include_directories(external) +include_directories(external/yajl/yajl-2.1.1/include) +include_directories(external/C-Mock/include/cmock) +include_directories(external/picojson) +include_directories(core/include/general) +include_directories(core/include/internal) +include_directories(core/include/services_sdk/interfaces) +include_directories(core/include/services_sdk/resources) +include_directories(core/include/services_sdk/utilities) +include_directories(core/include/attachments) +include_directories(components/include) + +add_subdirectory(build_system) +add_subdirectory(external) +add_subdirectory(core) +add_subdirectory(attachments) +add_subdirectory(components) +add_subdirectory(nodes) diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..8722099 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,47 @@ +# Contributor Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors pledge to make participation in our community (project) a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience +* Focusing on what is best not just for us as individuals, but for the overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others’ private information, such as a physical or email address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting our team at [opensource@openappsec.io](mailto:opensource@openappsec.io). All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. + +The project team is obligated to respect the privacy and security of the reporter of any incident. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other leaders of this community. + +## Attribution + +This Code of Conduct is partially adapted from the [Contributor Covenant](https://contributor-covenant.org) Code of Conduct with language adopted from various versions. \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..3afdef7 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# open-appsec Contributing Guide +Thank you for your interest in open-appsec. We welcome everyone that wishes to share their knowledge and expertise to enhance and expand the project. + +Read our [Code of Conduct](./CODE_OF_CONDUCT.md) to keep our community approachable and respectable. + +In this guide we will provide an overview of the various contribution options' guidelines - from reporting or fixing a bug, to suggesting an enhancement. + +## Reporting security vulnerabilities + +If you've found a vulnerability or a potential vulnerability in open-appsec please let us know at [security-alert@openappsec.io](mailto:security-alert@openappsec.io). We'll send a confirmation email to acknowledge your report within 24 hours, and we'll send an additional email when we've identified the issue positively or negatively. + +An internal process will be activated upon determining the validity of a reported security vulnerability, which will end with releasing a fix and deciding on the applicable disclosure actions. The reporter of the issue will receive updates of this process' progress. + +## Reporting a bug + +**Important - If the bug you wish to report regards a suspicion of a security vulnerability, please refer to the "Reporting security vulnerability" section** + +To report a bug, you can either open a new issue using a relevant [issue form](https://github.com/github/docs/issues/new/choose), or, alternatively, [contact us via our open-appsec open source distribution list](mailto:opensource@openappsec.io). + +Be sure to include a **title and clear description**, as much relevant information as possible, and a **code sample** or an **executable test case** demonstrating the expected behavior that is not occurring. + +## Contributing a fix to a bug + +Please [contact us via our open-appsec open source distribution list](mailto:opensource@openappsec.io) before writing your code. We will want to make sure we understand the boundaries of the proposed fix, that the relevant coding style is clear for the proposed fix's location in the code, and that the proposed contribution is relevant and eligible. + +## Proposing an enhancement + +Please [suggest your change via our open-appsec open source distribution list](mailto:opensource@openappsec.io) before writing your code. We will contact you to make sure we understand the boundaries of the proposed fix, that the relevant coding style is clear for the proposed fix's location in the code, and that the proposed contribution is relevant and eligible. There may be additional considerations that we would like to discuss with you before implementing the enhancement. + +## Open Source documentation issues + +For reporting or suggesting a change, of any issue detected in the documentation files of our open source repositories, please use the same guidelines as bug reports/fixes. + +# Final Thanks +We value all efforts to read, suggest changes and/or contribute to our open source files. Thank you for your time and efforts. + +The open-appsec Team diff --git a/LEXFO-CHP20221014-Report-Code_audit-OPEN-APPSEC-v1.2.pdf b/LEXFO-CHP20221014-Report-Code_audit-OPEN-APPSEC-v1.2.pdf new file mode 100644 index 0000000000000000000000000000000000000000..a98d78e7afa7d941361340d665797c18394c6789 GIT binary patch literal 978493 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
!P&rI!X>&ART|2=9$OLN<)r%?CC3J-AWR2Ceg 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

2RMWo6HH$9FZ5(=zQ_2gt(6}u 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�TwV3y^OTC2ZIXiy`4FfvG+&73 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ý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 khNwc$#{{t=- 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

qg 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

UzRNa;5k?enn|#YBgVje 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 W-d?Y_w_r(!tZOXfk-zK6A_Kg| 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�t!g}!Zls@mbW^J7C*-=K#3ixOI+ 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%0D3OC| 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[HEAD][TAIL]---> wrap + m_recentIdx++; + + if (m_recentIdx >= m_hitsPerSecond.size()) { + m_recentIdx = 0; + } + + // forget the hits from the oldest second in this interval (deduct them from total count) + m_hitsCount -= m_hitsPerSecond[m_recentIdx]; + m_hitsPerSecond[m_recentIdx] = 0; + + // Update recentHitTime (switch to next second) + m_recentHitTime += std::chrono::seconds(1); + } + + // increment hitcount in the most recent second's slot, and also the total count + m_hitsPerSecond[m_recentIdx]++; + m_hitsCount++; + return m_hitsCount <= m_max_events; +} + +} +} diff --git a/components/security_apps/waap/waap_clib/RateLimiter.h b/components/security_apps/waap/waap_clib/RateLimiter.h new file mode 100755 index 0000000..04c5942 --- /dev/null +++ b/components/security_apps/waap/waap_clib/RateLimiter.h @@ -0,0 +1,42 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include + +namespace Waap { +namespace Util { + +// Simple rate limiter primitive that collects events() and only allows up to X events per Y seconds. +// For each event, call RateLimiter::event() passing real or simulated timestamp (in seconds). +// The returned boolean value will tell the caller whether this event must pass (true) or be blocked (false). + +class RateLimiter { +public: + RateLimiter(unsigned int events, std::chrono::seconds interval); + void clear(const std::chrono::seconds& now); + bool event(const std::chrono::seconds& now); + +private: + + unsigned m_max_events; // max events allowed during the recent interval window + std::chrono::seconds m_interval; // configured interval window + std::vector m_hitsPerSecond; // array of hitcounts per second (remembers up to interval recent seconds) + unsigned m_recentIdx; // index of recent second + std::chrono::seconds m_recentHitTime; // timestamp of recent second + unsigned m_hitsCount; // total events during last interval seconds (rolling update) +}; + +} +} diff --git a/components/security_apps/waap/waap_clib/RateLimiting.cc b/components/security_apps/waap/waap_clib/RateLimiting.cc new file mode 100755 index 0000000..fa2ef15 --- /dev/null +++ b/components/security_apps/waap/waap_clib/RateLimiting.cc @@ -0,0 +1,255 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include + +#include "RateLimiting.h" +#include "Waf2Engine.h" +#include "agent_core_utilities.h" + +#define RATE_LIMITING_LRU_SIZE 10000 + +namespace Waap { +namespace RateLimiting { + +bool Policy::getRateLimitingEnforcementStatus() +{ + return m_rateLimiting.enable; +} + +bool +EntryKey::operator==(EntryKey const& other) const +{ + return url == other.url && source == other.source; +} + +bool +Policy::RateLimitingEnforcement::operator==(const Policy::RateLimitingEnforcement &other) const +{ + return enable == other.enable; +} + +bool +Policy::operator==(const Policy &other) const { + return rules == other.rules && m_rateLimiting == other.m_rateLimiting; +} + +bool +Policy::Rule::operator==(const Policy::Rule &other) const { + return action == other.action && rate == other.rate && + sourceFilter == other.sourceFilter && uriFilter == other.uriFilter; +} + +bool +Policy::Rule::Action::operator==(const Policy::Rule::Action &other) const { + return quarantineTimeSeconds == other.quarantineTimeSeconds && + type == other.type; +} + +bool +Policy::Rule::Rate::operator==(const Policy::Rule::Rate &other) const { + return events == other.events && interval == other.interval; +} + +bool +Policy::Rule::SourceFilter::operator==(const Policy::Rule::SourceFilter &other) const { + if (!(groupBy == other.groupBy && scope == other.scope)) + { + return false; + } + + if (specific_source_regexes_pattern.size() != other.specific_source_regexes_pattern.size()) + { + return false; + } + + for(size_t i=0; i= quarantinedUntil) { + // Release blocking state + state = TrackEntry::State::MEASURING; + } + } + + // Count this event, the result will be true if rate limiter not saturated (should allow), or false if it + // is (should block). + return eventRateLimiter.event(now); +} + +void +TrackEntry::quarantineUntil(std::chrono::seconds until) +{ + state = TrackEntry::State::QUARANTINED; + quarantinedUntil = until; +} + +bool +TrackEntry::isBlocked() const +{ + return state != TrackEntry::State::MEASURING; +} + +State::State(const std::shared_ptr &policy) +:policy(policy), perRuleTrackingTable() +{ + // For each rule create separate rate limiter states tracking table + for (unsigned ruleId=0; ruleId < policy->rules.size(); ++ruleId) { + perRuleTrackingTable.push_back(std::make_shared(RATE_LIMITING_LRU_SIZE)); + } +} + +static bool +matchOneOfRegexes(const std::string& value, const std::vector> ®exesList) +{ + for (auto ®ex : regexesList) { + if (regex != nullptr && NGEN::Regex::regexMatch(__FILE__, __LINE__, value, *regex)) { + return true; + } + } + + return false; +} + +bool +State::execute(const std::string& sourceIdentifier, const std::string& uriStr, std::chrono::seconds now, bool& log) +{ + bool allow = true; + log = false; + + // Run rules one by one. + for (unsigned ruleId=0; ruleId < policy->rules.size(); ++ruleId) { + const Policy::Rule &rule = policy->rules[ruleId]; + const Policy::Rule::UriFilter &uriFilter = rule.uriFilter; + const Policy::Rule::SourceFilter &sourceFilter = rule.sourceFilter; + const Policy::Rule::Rate &rate = rule.rate; + const Policy::Rule::Action &action = rule.action; + + // Get rate limiter states tracking table specific to current rule + std::shared_ptr table = perRuleTrackingTable[ruleId]; + + // Build a key to look up an entry + EntryKey entryKey; + + // Filter out unmatched Urls + if (uriFilter.scope == Waap::RateLimiting::Policy::Rule::UriFilter::Scope::SPECIFIC + && !matchOneOfRegexes(uriStr, uriFilter.specific_uri_regexes)) + { + continue; + } + + // Filter out unmatched Sources + if (sourceFilter.scope == Waap::RateLimiting::Policy::Rule::SourceFilter::Scope::SPECIFIC + && !matchOneOfRegexes(sourceIdentifier, sourceFilter.specific_source_regexes)) + { + continue; + } + + if (uriFilter.groupBy == Policy::Rule::UriFilter::GroupBy::URL) { + // Include the HTTP source ID in the key + entryKey.url = uriStr; + } + + if (sourceFilter.groupBy == Policy::Rule::SourceFilter::GroupBy::SOURCE) { + // Include the HTTP source ID in the key + entryKey.source = sourceIdentifier; + } + + // Find entry in LRU, or create new + std::shared_ptr trackEntry; + if (!table->get(entryKey, trackEntry)) { + trackEntry = std::make_shared(rate.events, std::chrono::seconds(rate.interval)); + } + + // Insert or update an entry in LRU (this moves entry up if exist, or inserts new, possibly expiring old ones + // to keep the LRU size under control). + table->insert(std::make_pair(entryKey, trackEntry)); + + // Count this event in the entry's rate limiter. Release temporary block if time arrived. + if (trackEntry->event(now) == false) { + // TrackEntry's rate limiter is saturated (too many requests) - act according to rule's Action + switch (action.type) { + case Policy::Rule::Action::Type::DETECT: + // log block action. + log = true; + // Detect + break; + case Policy::Rule::Action::Type::QUARANTINE: + // Mark this entry blocked temorarily, for at least X seconds + trackEntry->quarantineUntil(now + std::chrono::seconds(action.quarantineTimeSeconds)); + break; + case Policy::Rule::Action::Type::RATE_LIMIT: + // log block action. + log = true; + // Block this event only + allow = false; + break; + } + } + + // If at least one of the rules says "block" - block the request + if (trackEntry->isBlocked()) { + // log block action. + log = true; + allow = false; + } + } + + return allow; +} + +} +} diff --git a/components/security_apps/waap/waap_clib/RateLimiting.h b/components/security_apps/waap/waap_clib/RateLimiting.h new file mode 100755 index 0000000..05e53d4 --- /dev/null +++ b/components/security_apps/waap/waap_clib/RateLimiting.h @@ -0,0 +1,337 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "lru_cache_map.h" +#include "RateLimiter.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class Waf2Transaction; + +namespace Waap { +namespace RateLimiting { + +struct Policy { + struct Rule { + struct UriFilter { + enum class GroupBy { + GLOBAL, + URL + }; + + enum class Scope { + ALL, + SPECIFIC + }; + + // Deserialize the Type enum + Scope strScopeToEnum(std::string const &value) + { + if (boost::iequals(value, "all")) { + return Scope::ALL; + } + else if (boost::iequals(value, "specific")) { + return Scope::SPECIFIC; + } + else { + throw cereal::Exception( + "Invalid value for RateLimiting::Policy::Rule::SourceFilter::GroupBy='" + value + "'"); + } + } + + // Deserialize the Type enum + GroupBy strGroupByToEnum(std::string const &value) + { + if (boost::iequals(value, "all uris")) { + return GroupBy::GLOBAL; + } + else if (boost::iequals(value, "single uri")) { + return GroupBy::URL; + } + else { + throw cereal::Exception( + "Invalid value for RateLimiting::Policy::Rule::SourceFilter::GroupBy='" + value + "'"); + } + } + + template + void serialize(_A &ar) + { + std::string groupByStr; + ar(cereal::make_nvp("groupBy", groupByStr)); + groupBy = strGroupByToEnum(groupByStr); + std::string scopeStr; + ar(cereal::make_nvp("scope", scopeStr)); + scope = strScopeToEnum(scopeStr); + + if(scope == Scope::SPECIFIC) + { + ar(cereal::make_nvp("specific_uris", specific_uri_regexes_pattern)); + specific_uri_regexes.clear(); + + for (auto &specific_uri_pattern : specific_uri_regexes_pattern) + { + specific_uri_regexes.push_back(std::make_shared(specific_uri_pattern)); + } + } + } + + bool operator==(const Policy::Rule::UriFilter &other) const; + + GroupBy groupBy; + std::vector> specific_uri_regexes; + std::vector specific_uri_regexes_pattern; + Scope scope; + }; + + struct SourceFilter { + enum class GroupBy { + GLOBAL, + SOURCE + }; + + enum class Scope { + ALL, + SPECIFIC + }; + + // Deserialize the Type enum + Scope strScopeToEnum(std::string const &value) + { + if (boost::iequals(value, "all")) { + return Scope::ALL; + } + else if (boost::iequals(value, "specific")) { + return Scope::SPECIFIC; + } + else { + throw cereal::Exception( + "Invalid value for RateLimiting::Policy::Rule::SourceFilter::GroupBy='" + value + "'"); + } + } + + // Deserialize the Type enum + GroupBy strToEnum(std::string const &value) + { + if (boost::iequals(value, "all sources")) { + return GroupBy::GLOBAL; + } + else if (boost::iequals(value, "single source")) { + return GroupBy::SOURCE; + } + else { + throw cereal::Exception( + "Invalid value for RateLimiting::Policy::Rule::SourceFilter::GroupBy='" + value + "'"); + } + } + + template + void serialize(_A &ar) { + std::string groupByStr; + ar(cereal::make_nvp("groupBy", groupByStr)); + groupBy = strToEnum(groupByStr); + + std::string scopeStr; + ar(cereal::make_nvp("scope", scopeStr)); + scope = strScopeToEnum(scopeStr); + + if(scope == Scope::SPECIFIC) + { + ar(cereal::make_nvp("specific_sources", specific_source_regexes_pattern)); + specific_source_regexes.clear(); + + for (auto &specific_source_pattern : specific_source_regexes_pattern) { + specific_source_regexes.push_back(std::make_shared(specific_source_pattern)); + } + } + } + + bool operator==(const Policy::Rule::SourceFilter &other) const; + + GroupBy groupBy; + std::vector> specific_source_regexes; + std::vector specific_source_regexes_pattern; + Scope scope; + }; + + struct Rate { + template + void serialize(_A &ar) { + ar(cereal::make_nvp("interval", interval)); + ar(cereal::make_nvp("events", events)); + } + + bool operator==(const Policy::Rule::Rate &other) const; + + unsigned interval; // Interval in seconds + unsigned events; // Events allowed during the interval + }; + + struct Action { + enum class Type { + DETECT, + QUARANTINE, + RATE_LIMIT + }; + + // Deserialize the Type enum + Type strToEnum(std::string const &value) + { + if (boost::iequals(value, "detect")) { + return Type::DETECT; + } + else if (boost::iequals(value, "quarantine")) { + return Type::QUARANTINE; + } + else if (boost::iequals(value, "rate limit")) { + return Type::RATE_LIMIT; + } + else { + throw cereal::Exception( + "Invalid value for RateLimiting::Policy::Action::Type='" + value + "'"); + } + } + + template + void serialize(_A &ar) { + std::string typeStr; + ar(cereal::make_nvp("type", typeStr)); + type = strToEnum(typeStr); + quarantineTimeSeconds = 0; + if (type == Type::QUARANTINE) { + ar(cereal::make_nvp("quarantineTimeSeconds", quarantineTimeSeconds)); + } + } + + bool operator==(const Policy::Rule::Action &other) const; + + Type type; + unsigned quarantineTimeSeconds; // time to block (in seconds), relevant only for QUARANTINE action type + }; + + template + void serialize(_A &ar) { + ar(cereal::make_nvp("uriFilter", uriFilter)); + ar(cereal::make_nvp("sourceFilter", sourceFilter)); + ar(cereal::make_nvp("rate", rate)); + ar(cereal::make_nvp("action", action)); + } + + bool operator==(const Rule &other) const; + + UriFilter uriFilter; + SourceFilter sourceFilter; + Rate rate; + Action action; + }; + + class RateLimitingEnforcement + { + public: + RateLimitingEnforcement() + : + enable(false) + { + } + + template + RateLimitingEnforcement(_A &ar) + : + enable(false) + { + std::string level; + ar(cereal::make_nvp("rateLimitingEnforcement", level)); + level = boost::algorithm::to_lower_copy(level); + if (level == "prevent") { + enable = true; + } + } + + bool operator==(const Policy::RateLimitingEnforcement &other) const; + + bool enable; + }; + + std::vector rules; + RateLimitingEnforcement m_rateLimiting; + + Policy() {} + + bool getRateLimitingEnforcementStatus(); + bool operator==(const Policy &other) const; + + template + Policy(_A& ar) : m_rateLimiting(ar) { + ar(cereal::make_nvp("rateLimiting", rules)); + } + +}; + +// Key used to identify specific rate limiting entry +struct EntryKey { + std::string url; + std::string source; + // comparison operator should be implemented to use this struct as a key in an LRU cache. + bool operator==(EntryKey const& other) const; +}; + +// Support efficient hashing for the EntryKey struct so it can participate in unordered (hashed) containers such as LRU +inline std::size_t hash_value(EntryKey const &entryKey) +{ + std::size_t hash = 0; + boost::hash_combine(hash, entryKey.url); + boost::hash_combine(hash, entryKey.source); + return hash; +} + +// Rate limiting tracking entry +struct TrackEntry { + enum State { + MEASURING, + QUARANTINED + }; + Waap::Util::RateLimiter eventRateLimiter; + State state; + std::chrono::seconds quarantinedUntil; + TrackEntry(unsigned int events, std::chrono::seconds interval); + bool event(std::chrono::seconds now); + void quarantineUntil(std::chrono::seconds until); + bool isBlocked() const; +}; + +// Rate limiting state maintained per asset +class State { + public: + typedef LruCacheMap> EntriesLru; + const std::shared_ptr policy; + // For each rule - hold corresponding tracking state (EntriesLru) instance + std::vector> perRuleTrackingTable; + State(const std::shared_ptr &policy); + bool execute( + const std::string& sourceIdentifier, + const std::string& uriStr, + std::chrono::seconds now, + bool& log); +}; + +} +} diff --git a/components/security_apps/waap/waap_clib/RateLimitingDecision.cc b/components/security_apps/waap/waap_clib/RateLimitingDecision.cc new file mode 100755 index 0000000..bc95f59 --- /dev/null +++ b/components/security_apps/waap/waap_clib/RateLimitingDecision.cc @@ -0,0 +1,22 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "RateLimitingDecision.h" + +RateLimitingDecision::RateLimitingDecision(DecisionType type) : SingleDecision(type) +{} + +std::string RateLimitingDecision::getTypeStr() const +{ + return "Rate Limiting"; +} diff --git a/components/security_apps/waap/waap_clib/RateLimitingDecision.h b/components/security_apps/waap/waap_clib/RateLimitingDecision.h new file mode 100755 index 0000000..90242ab --- /dev/null +++ b/components/security_apps/waap/waap_clib/RateLimitingDecision.h @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __RATE_LIMITING_DECISION_H__ +#define __RATE_LIMITING_DECISION_H__ + +#include "SingleDecision.h" +#include "DecisionType.h" +#include + +class RateLimitingDecision: public SingleDecision +{ +public: + explicit RateLimitingDecision(DecisionType type); + std::string getTypeStr() const override; +}; +#endif diff --git a/components/security_apps/waap/waap_clib/ScanResult.cc b/components/security_apps/waap/waap_clib/ScanResult.cc new file mode 100755 index 0000000..0159c83 --- /dev/null +++ b/components/security_apps/waap/waap_clib/ScanResult.cc @@ -0,0 +1,79 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ScanResult.h" + +Waf2ScanResult::Waf2ScanResult() +: +keyword_matches(), +regex_matches(), +filtered_keywords(), +found_patterns(), +unescaped_line(), +param_name(), +location(), +score(0.0f), +scoreArray(), +keywordCombinations(), +attack_types(), +m_isAttackInParam(false) +{ +} + +void Waf2ScanResult::clear() +{ + keyword_matches.clear(); + regex_matches.clear(); + filtered_keywords.clear(); + found_patterns.clear(); + unescaped_line.clear(); + param_name.clear(); + location.clear(); + score = 0; + scoreArray.clear(); + keywordCombinations.clear(); + attack_types.clear(); +} + +void Waf2ScanResult::mergeFrom(const Waf2ScanResult& other) +{ + location = other.location; + param_name = other.param_name; + + Waap::Util::mergeFromVectorWithoutDuplicates( + other.keyword_matches, + keyword_matches + ); + Waap::Util::mergeFromVectorWithoutDuplicates( + other.regex_matches, + regex_matches + ); + Waap::Util::mergeFromMapOfVectorsWithoutDuplicates( + other.found_patterns, + found_patterns + ); + if (unescaped_line.empty()) + { + unescaped_line = other.unescaped_line; + } + + unescaped_line = other.unescaped_line + "?" + unescaped_line; + + + Waap::Util::mergeFromVectorWithoutDuplicates( + other.scoreArray, + scoreArray + ); + + attack_types.insert(other.attack_types.begin(), other.attack_types.end()); +} diff --git a/components/security_apps/waap/waap_clib/ScanResult.h b/components/security_apps/waap/waap_clib/ScanResult.h new file mode 100755 index 0000000..e923fcb --- /dev/null +++ b/components/security_apps/waap/waap_clib/ScanResult.h @@ -0,0 +1,41 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __SCAN_RESULT_H__ +#define __SCAN_RESULT_H__ + +#include "Waf2Util.h" +#include +#include +#include + + +struct Waf2ScanResult { + std::vector keyword_matches; + std::vector regex_matches; + std::vector filtered_keywords; + Waap::Util::map_of_stringlists_t found_patterns; + std::string unescaped_line; + std::string param_name; + std::string location; + double score; + std::vector scoreArray; + std::vector keywordCombinations; + std::set attack_types; + bool m_isAttackInParam; + void clear(); // clear Waf2ScanResult + Waf2ScanResult(); + void mergeFrom(const Waf2ScanResult& other); +}; + +#endif // __SCAN_RESULT_H__ diff --git a/components/security_apps/waap/waap_clib/ScannerDetector.cc b/components/security_apps/waap/waap_clib/ScannerDetector.cc new file mode 100755 index 0000000..1688042 --- /dev/null +++ b/components/security_apps/waap/waap_clib/ScannerDetector.cc @@ -0,0 +1,235 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ScannersDetector.h" +#include "waap.h" +#include "i_messaging.h" +#include + +USE_DEBUG_FLAG(D_WAAP); +#define SYNC_WAIT_TIME std::chrono::seconds(300) // 5 minutes in seconds +#define INTERVAL std::chrono::minutes(120) +#define EQUAL_VALUES_COUNT_THRESHOLD 2 +#define MAX_RETENTION 5 + +ScannerDetector::ScannerDetector(const std::string& localPath, const std::string& remotePath, + const std::string &assetId) : + SerializeToLocalAndRemoteSyncBase(INTERVAL, SYNC_WAIT_TIME, + localPath + "/11.data", + (remotePath == "") ? remotePath : remotePath + "/ScannersDetector", + assetId, + "ScannerDetector") +{ + m_sources_monitor.push_front(SourceKeyValsMap()); +} + +bool ScannerDetector::ready() +{ + if (m_lastSync.count() == 0) + { + return false; + } + std::chrono::microseconds currentTime = Singleton::Consume::by()->getWalltime(); + return (currentTime - m_lastSync < m_interval / 2); +} + +std::vector* ScannerDetector::getSourcesToIgnore() +{ + return &m_sources; +} + +void ScannerDetector::log(const std::string& source, const std::string& key, Waap::Keywords::KeywordsSet& keywords) +{ + m_sources_monitor.front()[source][key].insert(keywords.begin(), keywords.end()); +} + +void ScannerDetector::loadParams(std::shared_ptr pParams) +{ + std::string interval = pParams->getParamVal("learnIndicators.intervalDuration", + std::to_string(INTERVAL.count())); + setInterval(std::chrono::minutes(std::stoul(interval))); + std::string remoteSyncStr = pParams->getParamVal("remoteSync", "true"); + setRemoteSyncEnabled(!boost::iequals(remoteSyncStr, "false")); +} + +class SourcesMonitorPost : public RestGetFile +{ +public: + SourcesMonitorPost(ScannerDetector::SourceKeyValsMap& _monitor) + : monitor(_monitor) + { + } + +private: + C2S_PARAM(ScannerDetector::SourceKeyValsMap, monitor) +}; + +class SourcesMonitorGet : public RestGetFile +{ +public: + SourcesMonitorGet() : monitor() + { + } + + Maybe getSourcesMonitor() + { + return monitor.get(); + } + +private: + S2C_PARAM(ScannerDetector::SourceKeyValsMap, monitor) +}; + + +bool ScannerDetector::postData() +{ + m_sources_monitor_backup = m_sources_monitor.front(); + m_sources_monitor.push_front(SourceKeyValsMap()); + std::string url = getPostDataUrl(); + + dbgTrace(D_WAAP) << "Sending the data to: " << url; + + SourcesMonitorPost currentWindow(m_sources_monitor_backup); + return sendNoReplyObjectWithRetry(currentWindow, + I_Messaging::Method::PUT, + url); +} + +void ScannerDetector::pullData(const std::vector& files) +{ + std::string url = getPostDataUrl(); + std::string sentFile = url.erase(0, url.find_first_of('/') + 1); + dbgTrace(D_WAAP) << "pulling files, skipping: " << sentFile; + for (auto file : files) + { + if (file == sentFile) + { + continue; + } + dbgTrace(D_WAAP) << "Pulling the file: " << file; + SourcesMonitorGet getMonitor; + sendObjectWithRetry(getMonitor, + I_Messaging::Method::GET, + getUri() + "/" + file); + + SourceKeyValsMap remoteMonitor = getMonitor.getSourcesMonitor().unpack(); + for (const auto& srcData : remoteMonitor) + { + for (const auto& keyData : srcData.second) + { + m_sources_monitor_backup[srcData.first][keyData.first].insert( + keyData.second.begin(), + keyData.second.end()); + } + } + // update the sources monitor in previous "time window" + auto temp = m_sources_monitor.front(); + m_sources_monitor.pop_front(); + m_sources_monitor.pop_front(); + m_sources_monitor.push_front(m_sources_monitor_backup); + m_sources_monitor.push_front(temp); + } +} + +void ScannerDetector::postProcessedData() +{ + +} + +void ScannerDetector::updateState(const std::vector&) +{ +} + +void ScannerDetector::pullProcessedData(const std::vector& files) +{ + (void)files; +} + +void ScannerDetector::mergeMonitors(SourceKeyValsMap& mergeTo, SourceKeyValsMap& mergeFrom) +{ + for (const auto& srcData : mergeFrom) + { + for (const auto& keyData : srcData.second) + { + dbgTrace(D_WAAP) << "merging src: " << srcData.first << ", key: " << keyData.first << + ", keywords: " << Waap::Util::setToString(keyData.second); + mergeTo[srcData.first][keyData.first].insert(keyData.second.begin(), keyData.second.end()); + } + } +} + +void ScannerDetector::processData() +{ + if (m_sources_monitor_backup.empty()) + { + m_sources_monitor_backup = m_sources_monitor.front(); + m_sources_monitor.push_front(SourceKeyValsMap()); + } + + if (m_sources_monitor.size() > 2) + { + auto monitorItr = m_sources_monitor.begin()++; + for (monitorItr++; monitorItr != m_sources_monitor.end(); monitorItr++) + { + mergeMonitors(m_sources_monitor_backup, *monitorItr); + } + } + + m_sources.clear(); + for (auto source : m_sources_monitor_backup) + { + if (source.second.size() <= 2) + { + continue; + } + std::map>& keyVals = source.second; + for (auto key = keyVals.begin(); key != keyVals.end(); key++) + { + auto otherKey = key; + int counter = 0; + for (++otherKey; otherKey != keyVals.end(); otherKey++) + { + if (key->second != otherKey->second) + { + continue; + } + dbgTrace(D_WAAP) << "source monitor: src: " << source.first << ", key_1: " << key->first << ", key_2: " + << otherKey->first << ", vals: " << Waap::Util::setToString(key->second); + counter++; + } + if (counter >= EQUAL_VALUES_COUNT_THRESHOLD) + { + dbgDebug(D_WAAP) << "source: " << source.first << " will be ignored"; + m_sources.push_back(source.first); + break; + } + } + } + + if (m_sources_monitor.size() > MAX_RETENTION) + { + m_sources_monitor.pop_back(); + } + m_sources_monitor_backup.clear(); + m_lastSync = Singleton::Consume::by()->getWalltime(); +} + +void ScannerDetector::serialize(std::ostream& stream) +{ + (void)stream; +} + +void ScannerDetector::deserialize(std::istream& stream) +{ + (void)stream; +} diff --git a/components/security_apps/waap/waap_clib/ScannersDetector.h b/components/security_apps/waap/waap_clib/ScannersDetector.h new file mode 100755 index 0000000..e668e5c --- /dev/null +++ b/components/security_apps/waap/waap_clib/ScannersDetector.h @@ -0,0 +1,55 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __SCANNERS_DETECTOR_H__ +#define __SCANNERS_DETECTOR_H__ + +#include "WaapKeywords.h" +#include "i_serialize.h" +#include "i_ignoreSources.h" +#include "WaapParameters.h" + +class ScannerDetector : public SerializeToLocalAndRemoteSyncBase, public I_IgnoreSources +{ +public: + typedef std::map>> SourceKeyValsMap; + ScannerDetector(const std::string& localPath, const std::string& remotePath = "", const std::string &assetId = ""); + + virtual bool ready(); + virtual std::vector* getSourcesToIgnore(); + void log(const std::string& source, const std::string& key, Waap::Keywords::KeywordsSet& keywords); + + void loadParams(std::shared_ptr pParams); + + virtual bool postData(); + virtual void pullData(const std::vector& files); + virtual void processData(); + virtual void postProcessedData(); + virtual void pullProcessedData(const std::vector& files); + virtual void updateState(const std::vector& files); + + virtual void serialize(std::ostream& stream); + virtual void deserialize(std::istream& stream); + +private: + void mergeMonitors(SourceKeyValsMap& mergeTo, SourceKeyValsMap& mergeFrom); + + std::list m_sources_monitor; // list of map source -> key -> set of indicators + SourceKeyValsMap m_sources_monitor_backup; // stores data of the last window to process + + std::vector m_sources; + std::chrono::microseconds m_lastSync; +}; + + +#endif diff --git a/components/security_apps/waap/waap_clib/ScoreBuilder.cc b/components/security_apps/waap/waap_clib/ScoreBuilder.cc new file mode 100755 index 0000000..92fb58a --- /dev/null +++ b/components/security_apps/waap/waap_clib/ScoreBuilder.cc @@ -0,0 +1,499 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ScoreBuilder.h" +#include "Waf2Regex.h" +#include +#include +#include +#include "WaapAssetState.h" +#include +#include +#include +#include +#include "debug.h" + +USE_DEBUG_FLAG(D_WAAP_SCORE_BUILDER); + +#define GENERATE_FALSE_POSITIVES_LIST_THRESHOLD 100 +#define SCORE_CALCULATION_THRESHOLD 5000 + +using namespace std::chrono; + +ScoreBuilderData::ScoreBuilderData() : + m_sourceIdentifier(), + m_userAgent(), + m_sample(), + m_relativeReputation(0.0), + m_fpClassification(UNKNOWN_TYPE) +{} + +ScoreBuilderData::ScoreBuilderData( + const std::string &sourceIdentifier, + const std::string &userAgent, + const std::string &sample, + double relativeReputation, + PolicyCounterType type, + const std::vector &keywordsMatches, + const std::vector &keywordsCombinations) + : + m_sourceIdentifier(sourceIdentifier), + m_userAgent(userAgent), + m_sample(sample), + m_relativeReputation(relativeReputation), + m_fpClassification(type), + m_keywordsMatches(keywordsMatches), + m_keywordsCombinations(keywordsCombinations) +{} + +KeywordsScorePool::KeywordsScorePool() +: +m_keywordsDataMap(), +m_stats() +{ +} + +void KeywordsScorePool::mergeScores(const KeywordsScorePool& baseScores) +{ + // find all keywords that exist in base but not in this + std::vector removedElements; + std::vector::iterator removedElementsIt; + for (KeywordDataMap::const_iterator it = m_keywordsDataMap.begin(); + it != m_keywordsDataMap.end(); ++it) + { + // key not found in base array + if (baseScores.m_keywordsDataMap.find(it->first) == baseScores.m_keywordsDataMap.end()) + { + removedElements.push_back(it->first); + } + } + + // removing elements that were deleted + for (removedElementsIt = removedElements.begin(); + removedElementsIt != removedElements.end(); + ++removedElementsIt) + { + m_keywordsDataMap.erase(*removedElementsIt); + } + + // learning new scores + for (KeywordDataMap::const_iterator it = baseScores.m_keywordsDataMap.begin(); + it != baseScores.m_keywordsDataMap.end(); ++it) + { + if (m_keywordsDataMap.find(it->first) == m_keywordsDataMap.end()) + { + m_keywordsDataMap[it->first] = it->second; + } + } +} + + +ScoreBuilder::ScoreBuilder(I_WaapAssetState* pWaapAssetState) : + SerializeToFilePeriodically(duration_cast(minutes(10)), pWaapAssetState->getSignaturesScoresFilePath()), + m_scoreTrigger(0), + m_fpStore(), + m_keywordsScorePools(), + m_falsePositivesSetsIntersection(), + m_pWaapAssetState(pWaapAssetState) +{ + restore(); +} + +ScoreBuilder::ScoreBuilder(I_WaapAssetState* pWaapAssetState, ScoreBuilder& baseScores) : + SerializeToFilePeriodically(duration_cast(minutes(10)), pWaapAssetState->getSignaturesScoresFilePath()), + m_scoreTrigger(0), + m_fpStore(), + m_keywordsScorePools(), + m_falsePositivesSetsIntersection(), + m_pWaapAssetState(pWaapAssetState) +{ + restore(); + + // merge + mergeScores(baseScores); +} + +void ScoreBuilder::serialize(std::ostream& stream) { + cereal::JSONOutputArchive archive(stream); + static const size_t version = 1; + archive( + cereal::make_nvp("version", version), + cereal::make_nvp("scorePools", m_keywordsScorePools) + ); +} + +void ScoreBuilder::deserialize(std::istream& stream) { + cereal::JSONInputArchive iarchive(stream); + + size_t version = 0; + try { + iarchive(cereal::make_nvp("version", version)); + } + catch (std::runtime_error & e) { + iarchive.setNextName(nullptr); + version = 0; + dbgDebug(D_WAAP_SCORE_BUILDER) << "ScoreBuilder version absent, using version " << version << + " e.what() is " << e.what(); + } + + dbgDebug(D_WAAP_SCORE_BUILDER) << "Loading scores from file version " << version << "..."; + + switch (version) + { + case 1: { + iarchive(cereal::make_nvp("scorePools", m_keywordsScorePools)); + break; + } + case 0: { + m_keywordsScorePools[KEYWORDS_SCORE_POOL_BASE] = KeywordsScorePool(iarchive); + break; + } + default: { + dbgDebug(D_WAAP_SCORE_BUILDER) << "Unknown scores file version: " << version; + } + } +} + +void ScoreBuilder::analyzeFalseTruePositive(ScoreBuilderData& data, const std::string &poolName, bool doBackup) +{ + if (data.m_fpClassification == UNKNOWN_TYPE) + { + dbgTrace(D_WAAP_SCORE_BUILDER) << + "analyzeFalseTruePositive(): Got UNKNOWN_TYPE as false positive classification " + ", will not pump keywords score"; + return; + } + dbgTrace(D_WAAP_SCORE_BUILDER) << "ScoreBuilder::analyzeFalseTruePositive: pumping score pool=" << poolName; + pumpKeywordScore(data, poolName, doBackup); +} + +bool ScoreBuilder::isHtmlContent(std::string sample) +{ + // count closing html elements + unsigned int closingHtmlElem = 0; + std::string::size_type pos = 0; + std::string htmlClosingElementHint = " 3) + { + return true; + } + + unsigned int openingHtmlElem = 0; + bool regexError = false; + std::string reName = "html opening element regex"; + Regex htmlOpenElementRe(" matches; + + if (sample.length() <= 30) + { + return false; + } + + openingHtmlElem = htmlOpenElementRe.findAllMatches(sample, matches); + + if (openingHtmlElem > 5) + { + return true; + } + return false; +} + +void ScoreBuilder::checkBadSourcesForLearning(double reputation, std::string& source, std::string& userAgent) +{ + if (m_fpStore.count == 0) + { + return; + } + m_fpStore.count++; + + if (reputation < 2.0) + { + if (m_fpStore.hasUaItem(userAgent)) + { + m_fpStore.uaItems.erase(userAgent); + } + if (m_fpStore.hasIpItem(source)) + { + m_fpStore.ipItems.erase(source); + } + } + + if (m_fpStore.count >= GENERATE_FALSE_POSITIVES_LIST_THRESHOLD) + { + m_fpStore.appendKeywordsSetsIntersectionToList(m_falsePositivesSetsIntersection); + m_fpStore.clear(); + } +} + +void ScoreBuilder::pumpKeywordScore(ScoreBuilderData& data, const std::string &poolName, bool doBackup) +{ + std::map::iterator poolIt = m_keywordsScorePools.find(poolName); + + if (poolIt == m_keywordsScorePools.end()) { + dbgDebug(D_WAAP_SCORE_BUILDER) << "pumpKeywordScore() is called with unknown poolName='" << poolName << + "'. Creating the pool."; + m_keywordsScorePools[poolName] = KeywordsScorePool(); + } + + poolIt = m_keywordsScorePools.find(poolName); + if (poolIt == m_keywordsScorePools.end()) { + dbgWarning(D_WAAP_SCORE_BUILDER) << "pumpKeywordScore() failed to create pool '" << poolName << "'"; + return; + } + + KeywordsScorePool &keywordsScorePool = poolIt->second; + + if (isHtmlContent(data.m_sample)) + { + dbgTrace(D_WAAP_SCORE_BUILDER) << "pumpKeywordScore: isHtmlContent -> do not process"; + return; + } + for (const std::string &keyword : data.m_keywordsMatches) { + pumpKeywordScorePerKeyword(data, keyword, KEYWORD_TYPE_KEYWORD, keywordsScorePool); + } + + for (const std::string &keyword : data.m_keywordsCombinations) { + pumpKeywordScorePerKeyword(data, keyword, KEYWORD_TYPE_COMBINATION, keywordsScorePool); + } + + if (doBackup && m_scoreTrigger >= SCORE_CALCULATION_THRESHOLD) + { + calcScore(poolName); + if (m_pWaapAssetState != NULL) + { + m_pWaapAssetState->updateScores(); + } + backupWorker(); + } +} + +void ScoreBuilder::calcScore(const std::string &poolName) +{ + std::map::iterator poolIt = m_keywordsScorePools.find(poolName); + + if (poolIt == m_keywordsScorePools.end()) { + dbgDebug(D_WAAP_SCORE_BUILDER) << "calcScore() is called with unknown poolName='" << poolName << + "'. Creating the pool."; + m_keywordsScorePools[poolName] = KeywordsScorePool(); + } + + poolIt = m_keywordsScorePools.find(poolName); + if (poolIt == m_keywordsScorePools.end()) { + dbgWarning(D_WAAP_SCORE_BUILDER) << "calcScore() failed to create pool '" << poolName << "'"; + return; + } + + KeywordsScorePool &keywordsScorePool = poolIt->second; + KeywordDataMap &keywordsDataMap = keywordsScorePool.m_keywordsDataMap; + KeywordsStats &keywordsStats = keywordsScorePool.m_stats; + + m_scoreTrigger = 0; + + for (auto fpKeyword : m_falsePositivesSetsIntersection) + { + if (keywordsDataMap.find(fpKeyword) == keywordsScorePool.m_keywordsDataMap.end()) + { + keywordsDataMap[fpKeyword]; + } + + keywordsDataMap[fpKeyword].falsePositiveCtr++; + keywordsStats.falsePositiveCtr++; + } + + m_falsePositivesSetsIntersection.clear(); + + KeywordDataMap newKeywordsDataMap; + + double tpAverageLog = log(keywordsStats.truePositiveCtr / std::max(keywordsDataMap.size(), (size_t)1) + 101); + for (auto keyword : keywordsDataMap) + { + double tpLog = log(keyword.second.truePositiveCtr + 1); + double tpScore = tpLog / (tpLog + tpAverageLog / 4 + 1); // range [0,1) + int fpAvg = 1; + keyword.second.score = 10 * tpScore * (fpAvg + 1) / (fpAvg + (keyword.second.falsePositiveCtr * 5) + 2); + + if (keyword.second.score > 1 || + keyword.second.falsePositiveCtr < 10 || + keyword.second.type == KEYWORD_TYPE_KEYWORD) + { + newKeywordsDataMap[keyword.first] = keyword.second; + } + } + keywordsDataMap = newKeywordsDataMap; +} + +void ScoreBuilder::snap() +{ + // Copy data from all mutable score pools to "snapshot" keyword->scores map + for (const std::pair &pool : m_keywordsScorePools) { + const std::string &poolName = pool.first; + const KeywordsScorePool& keywordScorePool = pool.second; + m_snapshotKwScoreMap[poolName]; + + for (const std::pair &kwData : keywordScorePool.m_keywordsDataMap) + { + const std::string &kwName = kwData.first; + double kwScore = kwData.second.score; + m_snapshotKwScoreMap[poolName][kwName] = kwScore; + } + } +} + +double ScoreBuilder::getSnapshotKeywordScore(const std::string &keyword, double defaultScore, + const std::string &poolName) const +{ + std::map::const_iterator poolIt = m_snapshotKwScoreMap.find(poolName); + if (poolIt == m_snapshotKwScoreMap.end()) { + dbgTrace(D_WAAP_SCORE_BUILDER) << "pool " << poolName << " does not exist. Getting score from base pool"; + poolIt = m_snapshotKwScoreMap.find(KEYWORDS_SCORE_POOL_BASE); + } + + if (poolIt == m_snapshotKwScoreMap.end()) { + dbgDebug(D_WAAP_SCORE_BUILDER) << + "base pool does not exist! This is probably a bug. Returning default score " << defaultScore; + return defaultScore; + } + + const KeywordScoreMap &kwScoreMap = poolIt->second; + + KeywordScoreMap::const_iterator kwScoreFound = kwScoreMap.find(keyword); + if (kwScoreFound == kwScoreMap.end()) { + dbgTrace(D_WAAP_SCORE_BUILDER) << "keywordScore:'" << keyword << "': " << defaultScore << + " (default, keyword not found in pool '" << poolName << "')"; + return defaultScore; + } + + dbgTrace(D_WAAP_SCORE_BUILDER) << "keywordScore:'" << keyword << "': " << kwScoreFound->second << " (pool '" << + poolName << "')"; + return kwScoreFound->second; +} + +keywords_set ScoreBuilder::getIpItemKeywordsSet(std::string ip) +{ + return m_fpStore.ipItems[ip]; +} + +keywords_set ScoreBuilder::getUaItemKeywordsSet(std::string userAgent) +{ + return m_fpStore.uaItems[userAgent]; +} + +unsigned int ScoreBuilder::getFpStoreCount() +{ + return m_fpStore.count; +} + +void ScoreBuilder::mergeScores(const ScoreBuilder& baseScores) +{ + for (const std::pair &pool : baseScores.m_keywordsScorePools) { + const std::string &poolName = pool.first; + if (m_keywordsScorePools.find(poolName) == m_keywordsScorePools.end()) { + m_keywordsScorePools[poolName]; + } + const KeywordsScorePool &baseKeywordsScorePool = pool.second; + m_keywordsScorePools[poolName].mergeScores(baseKeywordsScorePool); + } +} + +void ScoreBuilder::pumpKeywordScorePerKeyword(ScoreBuilderData& data, const std::string& keyword, + KeywordType keywordSource, KeywordsScorePool &keywordsScorePool) +{ + m_scoreTrigger++; + if (data.m_fpClassification == UNKNOWN_TYPE) { + dbgTrace(D_WAAP_SCORE_BUILDER) << + "pumpKeywordScorePerKeyword(): Got UNKNOWN_TYPE as false positive classifiaction " + ", will not pump keywords score"; + return; + } + + if (keywordsScorePool.m_keywordsDataMap.find(keyword) == keywordsScorePool.m_keywordsDataMap.end()) + { + keywordsScorePool.m_keywordsDataMap[keyword]; + } + KeywordData& keyData = keywordsScorePool.m_keywordsDataMap[keyword]; + keyData.type = keywordSource; + + if (data.m_fpClassification == TRUE_POSITIVE && keyData.score < 8) + { + dbgTrace(D_WAAP_SCORE_BUILDER) << + "pumpKeywordScorePerKeyword(): fpClassification = TRUE_POSITIVE for keyword: " << keyword; + keyData.truePositiveCtr++; + keywordsScorePool.m_stats.truePositiveCtr++; + } + else if (data.m_fpClassification == FALSE_POSITIVE && (keyData.score > 0.1 || keyData.truePositiveCtr < 10)) + { + dbgTrace(D_WAAP_SCORE_BUILDER) << + "pumpKeywordScorePerKeyword(): fpClassification = FALSE_POSITIVE for keyword: " << keyword; + m_fpStore.putFalsePositive(data.m_sourceIdentifier, data.m_userAgent, keyword); + } + +} + +void FalsePoisitiveStore::putFalsePositive(const std::string& ip, const std::string& userAgent, + const std::string& keyword) +{ + count = 1; + ipItems[ip].insert(keyword); + uaItems[userAgent].insert(keyword); +} + +bool FalsePoisitiveStore::hasIpItem(const std::string& ip) const +{ + return ipItems.find(ip) != ipItems.end(); +} + +bool FalsePoisitiveStore::hasUaItem(const std::string& ua) const +{ + return uaItems.find(ua) != uaItems.end(); +} + +void FalsePoisitiveStore::appendKeywordsSetsIntersectionToList(std::list& keywordsList) +{ + std::list ipKeywords; + std::unordered_set uaKeywords; + + for (auto ip : ipItems) { + for (auto keyword : ip.second) + { + ipKeywords.push_back(keyword); + } + } + + for (auto ua : uaItems) { + for (auto keyword : ua.second) + { + uaKeywords.insert(keyword); + } + } + + for (auto keyword : ipKeywords) + { + if (uaKeywords.find(keyword) != uaKeywords.end()) + { + keywordsList.push_back(keyword); + } + } +} + +void FalsePoisitiveStore::clear() +{ + count = 0; + ipItems.clear(); + uaItems.clear(); +} diff --git a/components/security_apps/waap/waap_clib/ScoreBuilder.h b/components/security_apps/waap/waap_clib/ScoreBuilder.h new file mode 100755 index 0000000..c817068 --- /dev/null +++ b/components/security_apps/waap/waap_clib/ScoreBuilder.h @@ -0,0 +1,173 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include "FpMitigation.h" +#include "Waf2Util.h" +#include "picojson.h" +#include "i_serialize.h" +#include +#include +#include + +struct ScoreBuilderData { + std::string m_sourceIdentifier; + std::string m_userAgent; + std::string m_sample; + double m_relativeReputation; + PolicyCounterType m_fpClassification; + std::vector m_keywordsMatches; + std::vector m_keywordsCombinations; + + ScoreBuilderData(); + ScoreBuilderData( + const std::string &sourceIdentifier, + const std::string &userAgent, + const std::string &sample, + double relativeReputation, + PolicyCounterType type, + const std::vector &keywordsMatches, + const std::vector &keywordsCombinations); +}; +enum KeywordType { + KEYWORD_TYPE_UNKNOWN, + KEYWORD_TYPE_KEYWORD, + KEYWORD_TYPE_COMBINATION +}; + +struct KeywordData { + KeywordData() : truePositiveCtr(0), falsePositiveCtr(0), score(0.0), type(KEYWORD_TYPE_UNKNOWN) {} + + unsigned int truePositiveCtr; + unsigned int falsePositiveCtr; + double score; + KeywordType type; + + template + void serialize(Archive& ar) { + ar(cereal::make_nvp("false_positives", falsePositiveCtr), + cereal::make_nvp("true_positives", truePositiveCtr), + cereal::make_nvp("score", score), + cereal::make_nvp("type", type)); + } +}; + +struct KeywordsStats { + KeywordsStats() : truePositiveCtr(0), falsePositiveCtr(0) {} + + template + void serialize(Archive& ar) { + ar(cereal::make_nvp("false_positives", falsePositiveCtr), + cereal::make_nvp("true_positives", truePositiveCtr)); + } + + unsigned int truePositiveCtr; + unsigned int falsePositiveCtr; +}; + +typedef std::unordered_set keywords_set; + +struct FalsePoisitiveStore { + unsigned int count; + std::unordered_map ipItems; + std::unordered_map uaItems; + + FalsePoisitiveStore() : count(0), ipItems(), uaItems() {} + void putFalsePositive(const std::string& ip, const std::string& userAgent, const std::string& keyword); + bool hasIpItem(const std::string& ip) const; + bool hasUaItem(const std::string& ua) const; + void appendKeywordsSetsIntersectionToList(std::list& keywordsList); + void clear(); +}; + +class I_WaapAssetState; + +typedef std::unordered_map KeywordDataMap; + +struct KeywordsScorePool { + KeywordDataMap m_keywordsDataMap; + KeywordsStats m_stats; + + KeywordsScorePool(); + + template + KeywordsScorePool(_A &iarchive) + { + KeywordDataMap tmpKeyordsDataMap; + iarchive(cereal::make_nvp("keyword_data", tmpKeyordsDataMap), + cereal::make_nvp("keyword_stats", m_stats)); + + // Decode keys (originally urlencoded in the source file) + for (auto item : tmpKeyordsDataMap) { + std::string key = item.first; + key.erase(unquote_plus(key.begin(), key.end()), key.end()); + m_keywordsDataMap[key] = item.second; + } + } + + template + void serialize(Archive& ar) { + ar( + cereal::make_nvp("keyword_data", m_keywordsDataMap), + cereal::make_nvp("keyword_stats", m_stats) + ); + } + + void mergeScores(const KeywordsScorePool& baseScores); +}; + +class ScoreBuilder : public SerializeToFilePeriodically { +public: + ScoreBuilder(I_WaapAssetState* pWaapAssetState); + ScoreBuilder(I_WaapAssetState* pWaapAssetState, ScoreBuilder& baseScores); + ~ScoreBuilder() {} + + void analyzeFalseTruePositive(ScoreBuilderData& data, const std::string &poolName, bool doBackup=true); + + bool isHtmlContent(std::string sample); + + void checkBadSourcesForLearning(double reputation, std::string& source, std::string& userAgent); + void pumpKeywordScore(ScoreBuilderData& data, const std::string &poolName, bool doBackup=true); + void calcScore(const std::string &poolName); + + void snap(); + double getSnapshotKeywordScore(const std::string &keyword, double defaultScore, const std::string &poolName) const; + + keywords_set getIpItemKeywordsSet(std::string ip); + keywords_set getUaItemKeywordsSet(std::string userAgent); + unsigned int getFpStoreCount(); + + virtual void serialize(std::ostream& stream); + virtual void deserialize(std::istream& stream); + + void mergeScores(const ScoreBuilder& baseScores); +protected: + typedef std::map KeywordScoreMap; + + void pumpKeywordScorePerKeyword(ScoreBuilderData& data, + const std::string& keyword, + KeywordType keywordSource, + KeywordsScorePool &keywordsScorePool); + + unsigned int m_scoreTrigger; + FalsePoisitiveStore m_fpStore; + std::map m_keywordsScorePools; // live data continuously updated during traffic + std::map m_snapshotKwScoreMap; // the snapshot is updated only by a call to snap() + std::list m_falsePositivesSetsIntersection; + I_WaapAssetState* m_pWaapAssetState; +}; diff --git a/components/security_apps/waap/waap_clib/SecurityHeadersPolicy.cc b/components/security_apps/waap/waap_clib/SecurityHeadersPolicy.cc new file mode 100644 index 0000000..860ade0 --- /dev/null +++ b/components/security_apps/waap/waap_clib/SecurityHeadersPolicy.cc @@ -0,0 +1,104 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "SecurityHeadersPolicy.h" +#include "Waf2Util.h" + +namespace Waap { +namespace SecurityHeaders { + +void +Policy::StrictTransportSecurity::buildInjectStr() { + if (preload && includeSubDomains) + { + directivesStr = "max-age=" + maxAge + "; includeSubDomains; preload"; + } + else if (includeSubDomains) + { + directivesStr = "max-age=" + maxAge + "; includeSubDomains"; + } + else if (preload) + { + directivesStr = "max-age=" + maxAge + "; preload"; + } + else + { + directivesStr = "max-age=" + maxAge; + } + headerDetails = std::make_pair(headerName, directivesStr); +} + +void +Policy::XFrameOptions::buildInjectStr() { + headerDetails = std::make_pair(headerName, directivesStr); +} + +void +Policy::XContentTypeOptions::buildInjectStr() { + headerDetails = std::make_pair(headerName, directivesStr); +} + +bool +Policy::SecurityHeadersEnforcement::operator==(const Policy::SecurityHeadersEnforcement &other) const +{ + return enable == other.enable; +} + +bool +Policy::XFrameOptions::operator==(const XFrameOptions &other) const +{ + return sameOrigin == other.sameOrigin && directivesStr == other.directivesStr && + deny == other.deny && headerName == other.headerName && + headerDetails.first == other.headerDetails.first && + headerDetails.second == other.headerDetails.second; +} + +bool +Policy::XContentTypeOptions::operator==(const XContentTypeOptions &other) const +{ + return directivesStr == other.directivesStr && headerName == other.headerName && + headerDetails.first == other.headerDetails.first && headerDetails.second == other.headerDetails.second; +} + +bool +Policy::StrictTransportSecurity::operator==(const StrictTransportSecurity &other) const +{ + return maxAge == other.maxAge && directivesStr == other.directivesStr && + includeSubDomains == other.includeSubDomains && headerName == other.headerName && + preload == other.preload && headerDetails.first == other.headerDetails.first && + headerDetails.second == other.headerDetails.second; +} + +bool +Policy::Headers::operator==(const Headers &other) const +{ + return other.headersInjectStr == headersInjectStr && hsts == other.hsts && + xContentTypeOptions == other.xContentTypeOptions && xFrameOptions == other.xFrameOptions; +} + +bool +Policy::operator==(const Policy &other) const +{ + return headers == other.headers && m_securityHeaders == other.m_securityHeaders; +} + +State::State(const std::shared_ptr &policy) +{ + for(auto headerStr : policy->headers.headersInjectStr) + { + headersInjectStrs.push_back(headerStr); + } +} + +} +} diff --git a/components/security_apps/waap/waap_clib/SecurityHeadersPolicy.h b/components/security_apps/waap/waap_clib/SecurityHeadersPolicy.h new file mode 100644 index 0000000..2782e2b --- /dev/null +++ b/components/security_apps/waap/waap_clib/SecurityHeadersPolicy.h @@ -0,0 +1,225 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include +#include +#include +#include +#include "debug.h" + +USE_DEBUG_FLAG(D_WAAP); + +namespace Waap { +namespace SecurityHeaders { +struct Policy { + struct StrictTransportSecurity { + void setDefaults() + { + maxAge = "31536000"; + includeSubDomains = true; + preload = false; + buildInjectStr(); + } + + template + void serialize(_A &ar) { + ar(cereal::make_nvp("maxAge", maxAge)); + ar(cereal::make_nvp("includeSubDomains", includeSubDomains)); + ar(cereal::make_nvp("preload", preload)); + buildInjectStr(); + } + + void buildInjectStr(); + bool operator==(const StrictTransportSecurity &other) const; + + const std::string headerName = "Strict-Transport-Security"; + std::string maxAge; + bool includeSubDomains; + bool preload; + std::string directivesStr; + // string that define exactly how the header should be inject after collecting all data. + std::pair headerDetails; + }; + + struct XFrameOptions { + + void setDefaults() + { + directivesStr = sameOrigin; + buildInjectStr(); + } + + template + void serialize(_A &ar) { + + std::string value; + ar(cereal::make_nvp("directive", value)); + if(boost::iequals(value, "sameOrigin")) + { + directivesStr = sameOrigin; + } + else if(boost::iequals(value, "deny")) + { + directivesStr = deny; + } + else + { + throw cereal::Exception( + "Invalid value for SecurityHeaders::Policy::XFrameOptions::directive='" + value + "'"); + } + + buildInjectStr(); + } + + void buildInjectStr(); + bool operator==(const XFrameOptions &other) const; + + const std::string sameOrigin = "SAMEORIGIN"; + const std::string deny = "DENY"; + const std::string headerName = "X-Frame-Options"; + std::string directivesStr; + // string that define exactly how the header should be inject after collecting all data. + std::pair headerDetails; + }; + + struct XContentTypeOptions + { + void setDefaults() + { + directivesStr = nosniff; + buildInjectStr(); + } + + template + void serialize(_A &ar) { + + std::string value; + ar(cereal::make_nvp("directive", value)); + if(boost::iequals(value, "nosniff")) + { + directivesStr = nosniff; + } + else + { + throw cereal::Exception( + "Invalid value for SecurityHeaders::Policy::XContentTypeOptions::directive='" + value + "'"); + } + + buildInjectStr(); + } + + void buildInjectStr(); + bool operator==(const XContentTypeOptions &other) const; + const std::string headerName = "X-Content-Type-Options"; + const std::string nosniff = "nosniff"; + std::string directivesStr; + // string that define exactly how the header should be inject after collecting all data. + std::pair headerDetails; + }; + + struct Headers { + + template + void serialize(_A &ar) { + try + { + ar(cereal::make_nvp("strictTransportSecurity", hsts)); + headersInjectStr.push_back( + std::make_pair(hsts.headerDetails.first, hsts.headerDetails.second)); + } + catch (std::runtime_error& e) + { + dbgTrace(D_WAAP) << "Strict-Transport-Security header is not configured. Loading defaults."; + hsts.setDefaults(); + headersInjectStr.push_back( + std::make_pair(hsts.headerDetails.first, hsts.headerDetails.second)); + } + try + { + ar(cereal::make_nvp("xFrameOptions", xFrameOptions)); + headersInjectStr.push_back( + std::make_pair(xFrameOptions.headerDetails.first, xFrameOptions.headerDetails.second)); + } + catch (std::runtime_error& e) + { + dbgTrace(D_WAAP) << "X-Frame-Options header is not configured. Loading defaults."; + xFrameOptions.setDefaults(); + headersInjectStr.push_back( + std::make_pair(xFrameOptions.headerDetails.first, xFrameOptions.headerDetails.second)); + } + try + { + ar(cereal::make_nvp("xContentTypeOptions", xContentTypeOptions)); + headersInjectStr.push_back( + std::make_pair(xContentTypeOptions.headerDetails.first, xContentTypeOptions.headerDetails.second)); + } + catch (std::runtime_error& e) + { + dbgTrace(D_WAAP) << "X Content Type Options header is not configured. Loading defaults."; + xContentTypeOptions.setDefaults(); + headersInjectStr.push_back( + std::make_pair(xContentTypeOptions.headerDetails.first, xContentTypeOptions.headerDetails.second)); + } + } + + bool operator==(const Headers &other) const; + // will contain all strings that should be injected as headers. + std::vector> headersInjectStr; + StrictTransportSecurity hsts; + XFrameOptions xFrameOptions; + XContentTypeOptions xContentTypeOptions; + }; + + class SecurityHeadersEnforcement + { + public: + template + SecurityHeadersEnforcement(_A &ar) + : + enable(false) + { + std::string level; + ar(cereal::make_nvp("securityHeadersEnforcement", level)); + level = boost::algorithm::to_lower_copy(level); + if (level == "prevent") { + enable = true; + } + } + + bool operator==(const Policy::SecurityHeadersEnforcement &other) const; + + bool enable; + }; + + Headers headers; + SecurityHeadersEnforcement m_securityHeaders; + + bool operator==(const Policy &other) const; + + template + Policy(_A& ar) : m_securityHeaders(ar) { + ar(cereal::make_nvp("securityHeaders", headers)); + } + +}; +class State { + public: + const std::shared_ptr policy; + State(const std::shared_ptr &policy); + std::vector> headersInjectStrs; +}; + +} +} diff --git a/components/security_apps/waap/waap_clib/Serializator.cc b/components/security_apps/waap/waap_clib/Serializator.cc new file mode 100755 index 0000000..71cf80a --- /dev/null +++ b/components/security_apps/waap/waap_clib/Serializator.cc @@ -0,0 +1,850 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "i_serialize.h" +#include "waap.h" +#include "Waf2Util.h" +#include "WaapAssetState.h" +#include "i_instance_awareness.h" +#include +#include +#include +#include "debug.h" +#include +#include +#include +#include "SyncLearningNotification.h" +#include "report_messaging.h" +#include "compression_utils.h" +#include "config.h" + +USE_DEBUG_FLAG(D_WAAP_CONFIDENCE_CALCULATOR); + +namespace ch = std::chrono; +using namespace std; +typedef ch::duration> days; + +// Define interval between successful sync times +static const ch::minutes assetSyncTimeSliceLength(10); +static const int remoteSyncMaxPollingAttempts = 10; +static const string defaultLearningHost = "appsec-learning-svc"; +static const string defaultSharedStorageHost = "appsec-shared-storage-svc"; + +#define SHARED_STORAGE_HOST_ENV_NAME "SHARED_STORAGE_HOST" +#define LEARNING_HOST_ENV_NAME "LEARNING_HOST" + +static bool +isGZipped(const std::string &stream) +{ + if (stream.size() < 2) return false; + auto unsinged_stream = reinterpret_cast(stream.data()); + return unsinged_stream[0] == 0x1f && unsinged_stream[1] == 0x8b; +} + +bool RestGetFile::loadJson(const std::string& json) +{ + std::string json_str = json; + if (isGZipped(json_str) == 0) + { + return ClientRest::loadJson(json_str); + } + auto compression_stream = initCompressionStream(); + DecompressionResult res = decompressData( + compression_stream, + json_str.size(), + reinterpret_cast(json_str.c_str())); + + if (res.ok){ + json_str = std::string((const char *)res.output, res.num_output_bytes); + if (res.output) free(res.output); + res.output = nullptr; + res.num_output_bytes = 0; + } + + finiCompressionStream(compression_stream); + return ClientRest::loadJson(json_str); +} + +Maybe RestGetFile::genJson() const +{ + Maybe json = ClientRest::genJson(); + if (json.ok()) + { + std::string data = json.unpack(); + auto compression_stream = initCompressionStream(); + CompressionResult res = compressData( + compression_stream, + CompressionType::GZIP, + data.size(), + reinterpret_cast(data.c_str()), + true); + finiCompressionStream(compression_stream); + if (!res.ok) { + dbgWarning(D_WAAP_CONFIDENCE_CALCULATOR) << "Failed to gzip data"; + return genError("Failed to compress data"); + } + data = std::string((const char *)res.output, res.num_output_bytes); + json = data; + + if (res.output) free(res.output); + res.output = nullptr; + res.num_output_bytes = 0; + } + return json; +} +SerializeToFilePeriodically::SerializeToFilePeriodically(std::chrono::seconds pollingIntervals, std::string filePath) : + SerializeToFileBase(filePath), + m_lastSerialization(0), + m_interval(pollingIntervals) +{ + I_TimeGet* timer = Singleton::Consume::by(); + + if (timer != NULL) + { + m_lastSerialization = timer->getMonotonicTime(); + } +} + +SerializeToFilePeriodically::~SerializeToFilePeriodically() +{ + +} + +void SerializeToFilePeriodically::backupWorker() +{ + I_TimeGet* timer = Singleton::Consume::by(); + auto currentTime = timer->getMonotonicTime(); + + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "backup worker: current time: " << currentTime.count(); + + if (currentTime - m_lastSerialization >= m_interval) + { + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "backup worker: backing up data"; + m_lastSerialization = currentTime; + // save data + saveData(); + + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "backup worker: data is backed up"; + } +} + +void SerializeToFilePeriodically::setInterval(std::chrono::seconds newInterval) +{ + if (m_interval != newInterval) + { + m_interval = newInterval; + I_TimeGet* timer = Singleton::Consume::by(); + m_lastSerialization = timer->getMonotonicTime(); + } +} + +SerializeToFileBase::SerializeToFileBase(std::string fileName) : m_filePath(fileName) +{ + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "SerializeToFileBase::SerializeToFileBase() fname='" << m_filePath + << "'"; +} + +SerializeToFileBase::~SerializeToFileBase() +{ + +} + +void SerializeToFileBase::saveData() +{ + std::fstream filestream; + + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "saving to file: " << m_filePath; + filestream.open(m_filePath, std::fstream::out); + + std::stringstream ss; + + if (filestream.is_open() == false) { + dbgWarning(D_WAAP_CONFIDENCE_CALCULATOR) << "failed to open file: " << m_filePath << " Error: " + << strerror(errno); + return; + } + + serialize(ss); + filestream << ss.str(); + filestream.close(); +} + +void SerializeToFileBase::loadFromFile(std::string filePath) +{ + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "loadFromFile() file: " << filePath; + std::fstream filestream; + + filestream.open(filePath, std::fstream::in); + + if (filestream.is_open() == false) { + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "failed to open file: " << filePath << " Error: " << + strerror(errno); + if (!Singleton::exists() || errno != ENOENT) + { + return; + } + // if we fail to open a file because it doesn't exist and instance awareness is present + // try to strip the unique ID from the path and load the file from the parent directory + // that might exist in previous run where instance awareness didn't exits. + I_InstanceAwareness* instanceAwareness = Singleton::Consume::by(); + Maybe id = instanceAwareness->getUniqueID(); + if (!id.ok()) + { + return; + } + std::string idStr = "/" + id.unpack() + "/"; + size_t idPosition = filePath.find(idStr); + if (idPosition != std::string::npos) + { + filePath.erase(idPosition, idStr.length() - 1); + dbgDebug(D_WAAP_CONFIDENCE_CALCULATOR) << "retry to load file from : " << filePath; + loadFromFile(filePath); + } + return; + } + + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "loading from file: " << filePath; + + int length; + filestream.seekg(0, std::ios::end); // go to the end + length = filestream.tellg(); // report location (this is the length) + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "file length: " << length; + assert(length >= 0); // length -1 really happens if filePath is a directory (!) + char* buffer = new char[length]; // allocate memory for a buffer of appropriate dimension + filestream.seekg(0, std::ios::beg); // go back to the beginning + if (!filestream.read(buffer, length)) // read the whole file into the buffer + { + filestream.close(); + delete[] buffer; + dbgWarning(D_WAAP_CONFIDENCE_CALCULATOR) << "Failed to read file, file: " << filePath; + return; + } + filestream.close(); + + std::string dataObfuscated(buffer, length); + + delete[] buffer; + + std::stringstream ss(dataObfuscated); + + try + { + deserialize(ss); + } + catch (std::runtime_error & e) { + dbgWarning(D_WAAP_CONFIDENCE_CALCULATOR) << "failed to deserialize file: " << m_filePath << ", error: " << + e.what(); + } +} + +void SerializeToFileBase::restore() +{ + loadFromFile(m_filePath); +} + +void SerializeToFileBase::setFilePath(const std::string& new_file_path) +{ + m_filePath = new_file_path; +} + + +RemoteFilesList::RemoteFilesList() : files(), filesPathsList() +{ + +} + +// parses xml instead of json +// extracts a file list in +bool RemoteFilesList::loadJson(const std::string& xml) +{ + xmlDocPtr doc; // the resulting document tree + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "XML input: " << xml; + doc = xmlParseMemory(xml.c_str(), xml.length()); + + if (doc == NULL) { + dbgWarning(D_WAAP_CONFIDENCE_CALCULATOR) << "Failed to parse " << xml; + return false; + } + + xmlNodePtr node = doc->children; + if (node->children == NULL) + { + return false; + } + node = node->children; + + xmlChar *contents_name = xmlCharStrdup("Contents"); + xmlChar *key_name = xmlCharStrdup("Key"); + xmlChar *last_modified_name = xmlCharStrdup("LastModified"); + + // allows to get reference to the internal member and modify it + files.setActive(true); + while (node != NULL) + { + if (xmlStrEqual(contents_name, node->name) == 1) + { + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "Found the Contents element"; + xmlNodePtr contents_node = node->children; + std::string file; + std::string lastModified; + while (contents_node != NULL) + { + if (xmlStrEqual(key_name, contents_node->name) == 1) + { + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "Found the Key element"; + xmlChar* xml_file = xmlNodeGetContent(contents_node); + file = std::string(reinterpret_cast(xml_file)); + xmlFree(xml_file); + } + if (xmlStrEqual(last_modified_name, contents_node->name) == 1) + { + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "Found the LastModified element"; + xmlChar* xml_file = xmlNodeGetContent(contents_node); + lastModified = std::string(reinterpret_cast(xml_file)); + xmlFree(xml_file); + } + if (!file.empty() && !lastModified.empty()) + { + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "Adding the file: " << file << + " last modified: " << lastModified; + break; + } + contents_node = contents_node->next; + } + files.get().push_back(FileMetaData{ file, lastModified }); + filesPathsList.push_back(file); + } + node = node->next; + } + + // free up memory + xmlFree(last_modified_name); + xmlFree(contents_name); + xmlFree(key_name); + xmlFreeDoc(doc); + return true; +} + +const std::vector& RemoteFilesList::getFilesList() const +{ + return filesPathsList; +} + +const std::vector& RemoteFilesList::getFilesMetadataList() const +{ + return files.get(); +} + + +SerializeToLocalAndRemoteSyncBase::SerializeToLocalAndRemoteSyncBase( + std::chrono::minutes interval, + std::chrono::seconds waitForSync, + const std::string& filePath, + const std::string& remotePath, + const std::string& assetId, + const std::string& owner) + : + SerializeToFileBase(filePath), + m_remotePath(remotePath), + m_interval(0), + m_owner(owner), + m_pMainLoop(nullptr), + m_waitForSync(waitForSync), + m_workerRoutineId(0), + m_daysCount(0), + m_windowsCount(0), + m_intervalsCounter(0), + m_remoteSyncEnabled(true), + m_assetId(assetId), + m_shared_storage_host(genError("not set")), + m_learning_host(genError("not set")) +{ + dbgInfo(D_WAAP_CONFIDENCE_CALCULATOR) << "Create SerializeToLocalAndRemoteSyncBase. assetId='" << assetId << + "', owner='" << m_owner << "'"; + + if (Singleton::exists() && + Singleton::Consume::by()->getOrchestrationMode() == + OrchestrationMode::HYBRID) { + char* sharedStorageHost = getenv(SHARED_STORAGE_HOST_ENV_NAME); + if (sharedStorageHost != NULL) { + m_shared_storage_host = string(sharedStorageHost); + } else { + dbgWarning(D_WAAP_CONFIDENCE_CALCULATOR) << + "shared storage host name(" << + SHARED_STORAGE_HOST_ENV_NAME << + ") is not set"; + } + char* learningHost = getenv(LEARNING_HOST_ENV_NAME); + if (learningHost != NULL) { + m_learning_host = string(learningHost); + } else { + dbgWarning(D_WAAP_CONFIDENCE_CALCULATOR) << + "learning host name(" << + SHARED_STORAGE_HOST_ENV_NAME << + ") is not set"; + } + } + if (remotePath != "") { + // remote path is /// + auto parts = split(remotePath, '/'); + if (parts.size() > 2) { + size_t offset = 0; + if (parts[0].empty()) { + offset = 1; + } + std::string type = ""; + for (size_t i = offset + 2; i < parts.size(); i++) + { + type += type.empty() ? parts[i] : "/" + parts[i]; + } + m_type = type; + } + } + m_pMainLoop = Singleton::Consume::by(); + setInterval(interval); +} + +bool SerializeToLocalAndRemoteSyncBase::isBase() +{ + return m_remotePath == ""; +} + +std::string SerializeToLocalAndRemoteSyncBase::getUri() +{ + static const string hybridModeUri = "/api"; + static const string onlineModeUri = "/storage/waap"; + if (Singleton::exists() && + Singleton::Consume::by()->getOrchestrationMode() == + OrchestrationMode::HYBRID) return hybridModeUri; + return onlineModeUri; +} + +size_t SerializeToLocalAndRemoteSyncBase::getIntervalsCount() +{ + return m_intervalsCounter; +} + +SerializeToLocalAndRemoteSyncBase::~SerializeToLocalAndRemoteSyncBase() +{ + +} + +std::string SerializeToLocalAndRemoteSyncBase::getWindowId() +{ + return "window_" + std::to_string(m_daysCount) + "_" + std::to_string(m_windowsCount); +} + +std::string SerializeToLocalAndRemoteSyncBase::getPostDataUrl() +{ + std::string agentId = Singleton::Consume::by()->getAgentId(); + if (Singleton::exists()) + { + I_InstanceAwareness* instance = Singleton::Consume::by(); + Maybe uniqueId = instance->getUniqueID(); + if (uniqueId.ok()) + { + agentId += "/" + uniqueId.unpack(); + } + } + std::string windowId = getWindowId(); + return getUri() + "/" + m_remotePath + "/" + windowId + "/" + agentId + "/data.data"; +} +void SerializeToLocalAndRemoteSyncBase::setRemoteSyncEnabled(bool enabled) +{ + m_remoteSyncEnabled = enabled; +} + +void SerializeToLocalAndRemoteSyncBase::setInterval(std::chrono::seconds newInterval) +{ + dbgDebug(D_WAAP_CONFIDENCE_CALCULATOR) << "setInterval: from " << m_interval.count() << " to " << + newInterval.count() << " seconds. assetId='" << m_assetId << "', owner='" << m_owner << "'"; + + if (newInterval == m_interval) + { + return; + } + + m_interval = newInterval; + + if (m_workerRoutineId != 0) + { + return; + } + I_MainLoop::Routine syncRoutineOnLoad = [this]() { + I_TimeGet* timer = Singleton::Consume::by(); + ch::microseconds timeBeforeSyncWorker = timer->getWalltime(); + ch::microseconds timeAfterSyncWorker = timeBeforeSyncWorker; + while (true) + { + m_daysCount = ch::duration_cast(timeBeforeSyncWorker).count(); + + ch::microseconds timeSinceMidnight = timeBeforeSyncWorker - ch::duration_cast(timeBeforeSyncWorker); + m_windowsCount = timeSinceMidnight / m_interval; + + // Distribute syncWorker tasks for different assets spread over assetSyncTimeSliceLengthintervals + // It is guaranteed that for the same asset, sync events will start at the same time on all + // http_transaction_host instances. + size_t slicesCount = m_interval / assetSyncTimeSliceLength; + size_t sliceIndex = 0; + if (slicesCount != 0 && m_assetId != "") { + sliceIndex = std::hash{}(m_assetId) % slicesCount; + } + ch::seconds sliceOffset = assetSyncTimeSliceLength * sliceIndex; + + ch::microseconds remainingTime = m_interval - (timeAfterSyncWorker - timeBeforeSyncWorker) - + timeBeforeSyncWorker % m_interval + sliceOffset; + + if (remainingTime > m_interval) { + // on load between trigger and offset remaining time is larger than the interval itself + remainingTime -= m_interval; + dbgDebug(D_WAAP_CONFIDENCE_CALCULATOR) << "adjusting remaining time: " << remainingTime.count(); + if (timeBeforeSyncWorker.count() != 0) + { + auto updateTime = timeBeforeSyncWorker - m_interval; + m_daysCount = ch::duration_cast(updateTime).count(); + + ch::microseconds timeSinceMidnight = updateTime - ch::duration_cast(updateTime); + m_windowsCount = timeSinceMidnight / m_interval; + } + } + + if (remainingTime < ch::seconds(0)) { + // syncWorker execution time was so large the remaining time became negative + remainingTime = ch::seconds(0); + dbgError(D_WAAP_CONFIDENCE_CALCULATOR) << "syncWorker execution time (owner='" << m_owner << + "', assetId='" << m_assetId << "') is " << + ch::duration_cast(timeAfterSyncWorker - timeBeforeSyncWorker).count() << + " seconds, too long to cause negative remainingTime. Waiting 0 seconds..."; + } + + dbgDebug(D_WAAP_CONFIDENCE_CALCULATOR) << "current time: " << timeBeforeSyncWorker.count() << " \u00b5s" << + ": assetId='" << m_assetId << "'" << + ", owner='" << m_owner << "'" << + ", daysCount=" << m_daysCount << + ", windowsCount=" << m_windowsCount << + ", interval=" << m_interval.count() << " seconds" + ", seconds till next window=" << ch::duration_cast(remainingTime - sliceOffset).count() << + ", sliceOffset=" << sliceOffset.count() << " seconds" << + ", hashIndex=" << sliceIndex << + ": next wakeup in " << ch::duration_cast(remainingTime).count() << " seconds"; + m_pMainLoop->yield(remainingTime); + + timeBeforeSyncWorker = timer->getWalltime(); + syncWorker(); + timeAfterSyncWorker = timer->getWalltime(); + } + }; + m_workerRoutineId = m_pMainLoop->addOneTimeRoutine( + I_MainLoop::RoutineType::System, + syncRoutineOnLoad, + "Sync worker learning on load" + ); +} + +bool SerializeToLocalAndRemoteSyncBase::localSyncAndProcess() +{ + RemoteFilesList rawDataFiles; + + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "Getting files of all agents"; + + bool isSuccessful = sendObjectWithRetry(rawDataFiles, + I_Messaging::Method::GET, + getUri() + "/?list-type=2&prefix=" + m_remotePath + "/" + getWindowId() + "/"); + + if (!isSuccessful) + { + dbgError(D_WAAP_CONFIDENCE_CALCULATOR) << "Failed to get the list of files"; + return false; + } + + pullData(rawDataFiles.getFilesList()); + processData(); + saveData(); + postProcessedData(); + return true; +} + +std::chrono::seconds SerializeToLocalAndRemoteSyncBase::getIntervalDuration() const +{ + return m_interval; +} + +void SerializeToLocalAndRemoteSyncBase::updateStateFromRemoteService() +{ + for (int i = 0; i < remoteSyncMaxPollingAttempts; i++) + { + m_pMainLoop->yield(std::chrono::seconds(60)); + RemoteFilesList remoteFiles = getRemoteProcessedFilesList(); + if (remoteFiles.getFilesMetadataList().empty()) + { + dbgWarning(D_WAAP_CONFIDENCE_CALCULATOR) << "no files generated by the remote service were found"; + continue; + } + std::string lastModified = remoteFiles.getFilesMetadataList().begin()->modified; + if (lastModified != m_lastProcessedModified) + { + m_lastProcessedModified = lastModified; + updateState(remoteFiles.getFilesList()); + dbgInfo(D_WAAP_CONFIDENCE_CALCULATOR) << "Owner: " << m_owner << + ". updated state generated by remote at " << m_lastProcessedModified; + return; + } + } + dbgWarning(D_WAAP_CONFIDENCE_CALCULATOR) << "polling for update state timeout. for assetId='" + << m_assetId << "', owner='" << m_owner; + localSyncAndProcess(); +} + +void SerializeToLocalAndRemoteSyncBase::syncWorker() +{ + dbgInfo(D_WAAP_CONFIDENCE_CALCULATOR) << "Running the sync worker for assetId='" << m_assetId << "', owner='" << + m_owner << "'" << " last modified state: " << m_lastProcessedModified; + m_intervalsCounter++; + OrchestrationMode mode = Singleton::exists() ? + Singleton::Consume::by()->getOrchestrationMode() : OrchestrationMode::ONLINE; + + if (!m_remoteSyncEnabled || isBase() || !postData() || + mode == OrchestrationMode::OFFLINE) + { + dbgDebug(D_WAAP_CONFIDENCE_CALCULATOR) + << "Did not synchronize the data. Remote URL: " + << m_remotePath + << " is enabled: " + << std::to_string(m_remoteSyncEnabled); + processData(); + saveData(); + return; + } + + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "Waiting for all agents to post their data"; + m_pMainLoop->yield(m_waitForSync); + // check if learning service is operational + if (m_lastProcessedModified == "") + { + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "check if remote service is operational"; + RemoteFilesList remoteFiles = getRemoteProcessedFilesList(); + if (!remoteFiles.getFilesMetadataList().empty()) + { + m_lastProcessedModified = remoteFiles.getFilesMetadataList()[0].modified; + dbgInfo(D_WAAP_CONFIDENCE_CALCULATOR) << "First sync by remote service: " << m_lastProcessedModified; + } + } + + // check if learning service is enabled + bool isRemoteServiceEnabled = getProfileAgentSettingWithDefault( + true, + "appsecLearningSettings.remoteServiceEnabled"); + + dbgDebug(D_WAAP_CONFIDENCE_CALCULATOR) << "using remote service: " << isRemoteServiceEnabled; + if ((m_lastProcessedModified == "" || !isRemoteServiceEnabled) && !localSyncAndProcess()) + { + dbgWarning(D_WAAP_CONFIDENCE_CALCULATOR) << "local sync and process failed"; + return; + } + + if (mode == OrchestrationMode::HYBRID) { + dbgDebug(D_WAAP_CONFIDENCE_CALCULATOR) << "detected running in standalone mode"; + I_AgentDetails *agentDetails = Singleton::Consume::by(); + I_Messaging *messaging = Singleton::Consume::by(); + + SyncLearningObject syncObj(m_assetId, m_type, getWindowId()); + + Flags conn_flags; + conn_flags.setFlag(MessageConnConfig::EXTERNAL); + std::string tenant_header = "X-Tenant-Id: " + agentDetails->getTenantId(); + bool ok = messaging->sendNoReplyObject(syncObj, + I_Messaging::Method::POST, + getLearningHost(), + 80, + conn_flags, + "/api/sync", + tenant_header); + dbgDebug(D_WAAP_CONFIDENCE_CALCULATOR) << "sent learning sync notification ok: " << ok; + if (!ok) { + dbgWarning(D_WAAP_CONFIDENCE_CALCULATOR) << "failed to send learning notification"; + } + } else { + SyncLearningNotificationObject syncNotification(m_assetId, m_type, getWindowId()); + + dbgDebug(D_WAAP_CONFIDENCE_CALCULATOR) << "sending sync notification: " << syncNotification; + + ReportMessaging( + "sync notification for '" + m_assetId + "'", + ReportIS::AudienceTeam::WAAP, + syncNotification, + false, + MessageTypeTag::WAAP_LEARNING, + ReportIS::Tags::WAF, + ReportIS::Notification::SYNC_LEARNING + ); + } + + if (m_lastProcessedModified != "" && isRemoteServiceEnabled) + { + updateStateFromRemoteService(); + } +} + +void SerializeToLocalAndRemoteSyncBase::restore() +{ + SerializeToFileBase::restore(); + if (!isBase()) + { + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "merge state from remote service"; + mergeProcessedFromRemote(); + } +} + +RemoteFilesList SerializeToLocalAndRemoteSyncBase::getRemoteProcessedFilesList() +{ + RemoteFilesList remoteFiles; + bool isRemoteServiceEnabled = getProfileAgentSettingWithDefault( + true, + "appsecLearningSettings.remoteServiceEnabled"); + + if (!isRemoteServiceEnabled) + { + dbgDebug(D_WAAP_CONFIDENCE_CALCULATOR) << "remote service is disabled"; + return remoteFiles; + } + + bool isSuccessful = sendObject( + remoteFiles, + I_Messaging::Method::GET, + getUri() + "/?list-type=2&prefix=" + m_remotePath + "/remote"); + + if (!isSuccessful) + { + dbgWarning(D_WAAP_CONFIDENCE_CALCULATOR) << "Failed to get the list of files"; + } + return remoteFiles; +} + + +RemoteFilesList SerializeToLocalAndRemoteSyncBase::getProcessedFilesList() +{ + RemoteFilesList processedFilesList = getRemoteProcessedFilesList(); + + if (!processedFilesList.getFilesList().empty()) + { + const std::vector& filesMD = processedFilesList.getFilesMetadataList(); + if (filesMD.size() > 1) { + dbgWarning(D_WAAP_CONFIDENCE_CALCULATOR) << "got more than 1 expected processed file"; + } + if (!filesMD.empty()) { + m_lastProcessedModified = filesMD[0].modified; + } + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "found " << filesMD.size() << " remote service state files. " + "last modified: " << m_lastProcessedModified; + + return processedFilesList; + } + + + bool isSuccessful = sendObject( + processedFilesList, + I_Messaging::Method::GET, + getUri() + "/?list-type=2&prefix=" + m_remotePath + "/processed"); + + if (!isSuccessful) + { + dbgDebug(D_WAAP_CONFIDENCE_CALCULATOR) << "Failed to get the list of files"; + } + else if (!processedFilesList.getFilesList().empty()) + { + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "found state files"; + return processedFilesList; + } + // backward compatibility - try to get backup file with the buggy prefix tenantID/assetID/instanceID/ + std::string bcRemotePath = m_remotePath; + size_t pos = bcRemotePath.find('/'); + pos = bcRemotePath.find('/', pos + 1); + if (!Singleton::exists()) + { + dbgDebug(D_WAAP_CONFIDENCE_CALCULATOR) << "missing instance of instance awareness," + " can't check backward compatibility"; + return processedFilesList; + } + I_InstanceAwareness* instanceAwareness = Singleton::Consume::by(); + Maybe id = instanceAwareness->getUniqueID(); + if (!id.ok()) + { + dbgDebug(D_WAAP_CONFIDENCE_CALCULATOR) << "failed to get instance id err: " << id.getErr() << + ". can't check backward compatibility"; + return processedFilesList; + } + std::string idStr = id.unpack(); + bcRemotePath.insert(pos + 1, idStr + "/"); + dbgDebug(D_WAAP_CONFIDENCE_CALCULATOR) << "List of files is empty - trying to get the file from " << + bcRemotePath; + + isSuccessful = sendObject( + processedFilesList, + I_Messaging::Method::GET, + getUri() + "/?list-type=2&prefix=" + bcRemotePath + "/processed"); + + if (!isSuccessful) + { + dbgWarning(D_WAAP_CONFIDENCE_CALCULATOR) << "Failed to get the list of files"; + } + dbgDebug(D_WAAP_CONFIDENCE_CALCULATOR) << "backwards computability: got " + << processedFilesList.getFilesList().size() << " state files"; + return processedFilesList; +} + +void SerializeToLocalAndRemoteSyncBase::mergeProcessedFromRemote() +{ + dbgDebug(D_WAAP_CONFIDENCE_CALCULATOR) << "Merging processed data from remote. assetId='" << m_assetId << + "', owner='" << m_owner << "'"; + m_pMainLoop->addOneTimeRoutine( + I_MainLoop::RoutineType::Offline, + [&]() + { + RemoteFilesList processedFiles = getProcessedFilesList(); + pullProcessedData(processedFiles.getFilesList()); + }, + "Merge processed data from remote for asset Id: " + m_assetId + ", owner:" + m_owner + ); +} + +string +SerializeToLocalAndRemoteSyncBase::getLearningHost() +{ + if (m_learning_host.ok()) { + return *m_learning_host; + } else { + char* learningHost = getenv(LEARNING_HOST_ENV_NAME); + if (learningHost != NULL) { + m_learning_host = string(learningHost); + return learningHost; + } + dbgWarning(D_WAAP_CONFIDENCE_CALCULATOR) << "learning host is not set. using default"; + } + return defaultLearningHost; +} + +string +SerializeToLocalAndRemoteSyncBase::getSharedStorageHost() +{ + if (m_shared_storage_host.ok()) { + return *m_shared_storage_host; + } else { + char* sharedStorageHost = getenv(SHARED_STORAGE_HOST_ENV_NAME); + if (sharedStorageHost != NULL) { + m_shared_storage_host = string(sharedStorageHost); + return sharedStorageHost; + } + dbgWarning(D_WAAP_CONFIDENCE_CALCULATOR) << "shared storage host is not set. using default"; + } + return defaultSharedStorageHost; +} diff --git a/components/security_apps/waap/waap_clib/Signatures.cc b/components/security_apps/waap/waap_clib/Signatures.cc new file mode 100755 index 0000000..751efbb --- /dev/null +++ b/components/security_apps/waap/waap_clib/Signatures.cc @@ -0,0 +1,278 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "Signatures.h" +#include "i_encryptor.h" +#include "waap.h" +#include + +USE_DEBUG_FLAG(D_WAAP); + +typedef picojson::value::object JsObj; +typedef picojson::value JsVal; +typedef picojson::value::array JsArr; +typedef std::map> filtered_parameters_t; + + +static std::vector to_strvec(const picojson::value::array& jsV) +{ + std::vector r; + + for (auto it = jsV.begin(); it != jsV.end(); ++it) { + r.push_back(it->get()); + } + + return r; +} + +static std::set to_strset(const picojson::value::array& jsA) +{ + std::set r; + + for (auto it = jsA.begin(); it != jsA.end(); ++it) { + r.insert(it->get()); + } + + return r; +} + +static std::map to_regexmap(const picojson::value::object& jsO, bool& error) +{ + std::map r; + + for (auto it = jsO.begin(); it != jsO.end(); ++it) { + const std::string& n = it->first; + // convert name to lowercase now (so we don't need to do it at runtime every time). + std::string n_lower; + for (std::string::const_iterator pCh = n.begin(); pCh != n.end(); ++pCh) { + n_lower += std::tolower(*pCh); + } + const picojson::value& v = it->second; + + if (error) { + // stop loading regexes if there's previous error... + break; + } + + // Pointers to Regex instances are stored instead of instances themselves to avoid + // the need to make the Regex objects copyable. + // However, these pointers must be freed by the holder of the returned map! + // note: in our case this freeing is happening in the destructor of the WaapAssetState class. + r[n] = new Regex(v.get(), error, n_lower); + } + + return r; +} + +static filtered_parameters_t to_filtermap(const picojson::value::object& JsObj) +{ + filtered_parameters_t result; + for (auto it = JsObj.begin(); it != JsObj.end(); ++it) + { + const std::string parameter = it->first; + const picojson::value::array& arr = it->second.get(); + result[parameter] = to_strvec(arr); + } + return result; +} + +std::string genDelimitedKeyValPattern(const std::string& delim) +{ + std::string pattern = "^([^" + delim + "]+?=[^" + delim + "]+?" + delim + ")+" + "([^" + delim + "]+?=[^" + delim + "]+?)" + delim + "?$"; + return pattern; +} + +Signatures::Signatures(const std::string& filepath) : + sigsSource(loadSource(filepath)), + error(false), + m_regexPreconditions(std::make_shared(sigsSource, error)), + words_regex( + to_strvec(sigsSource["words_regex_list"].get()), + error, + "words_regex_list", + m_regexPreconditions + ), + specific_acuracy_keywords_regex( + to_strvec(sigsSource["specific_acuracy_keywords_regex_list"].get()), + error, + "specific_acuracy_keywords_regex_list", + m_regexPreconditions + ), + pattern_regex( + to_strvec(sigsSource["pattern_regex_list"].get()), + error, + "pattern_regex_list", + m_regexPreconditions + ), + un_escape_pattern(sigsSource["un_escape_pattern"].get(), error, "un_escape_pattern"), + quotes_ev_pattern(sigsSource["quotes_ev_pattern"].get(), error, "quotes_ev_pattern"), + comment_ev_pattern(sigsSource["comment_ev_pattern"].get(), error, "comment_ev_pattern"), + quotes_space_ev_pattern( + sigsSource["quotes_space_ev_fast_reg"].get(), error, + "quotes_space_ev_fast_reg" + ), + allowed_text_re(sigsSource["allowed_text_re"].get(), error, "allowed_text_re"), + pipe_split_re( + "([\\w\\=\\-\\_\\.\\,\\(\\)\\[\\]\\/\\%\\s]+?)\\||([\\w\\=\\-\\_\\.\\,\\(\\)\\[\\]\\/\\%\\s]+)|\\|()", + error, + "pipe_decode"), + semicolon_split_re("([\\w\\=\\-\\_\\.\\,\\(\\)\\%]+?);|([\\w\\=\\-\\_\\.\\,\\(\\)\\%]+)|;()", error, "sem_decode"), + longtext_re(sigsSource["longtext_re"].get(), error, "longtext_re"), + nospaces_long_value_re("^[^\\s]{16,}$", error, "nospaces_long_value_re"), + good_header_name_re(sigsSource["good_header_name_re"].get(), error, "good_header_name"), + good_header_value_re(sigsSource["good_header_value_re"].get(), error, "good_header_value"), + ignored_for_nospace_long_value( + to_strset(sigsSource["ignored_for_nospace_long_value"].get())), + global_ignored_keywords( + to_strset( + sigsSource["global_ignored"].get()["keys"].get() + ) + ), + global_ignored_patterns( + to_strset( + sigsSource["global_ignored"].get()["patterns"].get() + ) + ), + url_ignored_keywords( + to_strset( + sigsSource["ignored_for_url"].get()["keys"].get() + ) + ), + url_ignored_patterns( + to_strset( + sigsSource["ignored_for_url"].get()["patterns"].get() + ) + ), + url_ignored_re( + sigsSource["ignored_for_url"].get()["regex"].get(), + error, + "url_ignored" + ), + header_ignored_keywords( + to_strset( + sigsSource["ignored_for_headers"].get()["keys"].get() + ) + ), + header_ignored_patterns( + to_strset( + sigsSource["ignored_for_headers"].get() + ["patterns"].get() + ) + ), + header_ignored_re( + sigsSource["ignored_for_headers"].get()["regex"].get(), + error, + "header_ignored" + ), + filter_parameters( + to_filtermap( + sigsSource["filter_parameters"].get() + ) + ), + m_attack_types( + to_filtermap( + sigsSource["attack_types_map"].get() + ) + ), + // Removed by Pavel's request. Leaving here in case he'll want to add this back... +#if 0 + cookie_ignored_keywords( + to_strset( + sigsSource["ignored_for_cookies"].get()["keys"].get() + ) + ), + cookie_ignored_patterns( + to_strset( + sigsSource["ignored_for_cookies"].get() + ["patterns"].get() + ) + ), + cookie_ignored_re( + sigsSource["ignored_for_cookies"].get()["regex"].get(), + error, + "cookie_ignored" + ), +#endif + php_serialize_identifier("^(N;)|^([ibdsOoCcRra]:\\d+)", error, "php_serialize_identifier"), + html_regex("(<(?>body|head)\\b.*>(?>.|[\\r\\n]){0,400}){2}|.+\\|)+.+}"), + pipes_delimited_key_val_re(genDelimitedKeyValPattern("\\|")), + semicolon_delimited_key_val_re(genDelimitedKeyValPattern(";")), + asterisk_delimited_key_val_re(genDelimitedKeyValPattern("\\*")), + comma_delimited_key_val_re(genDelimitedKeyValPattern(",")), + ampersand_delimited_key_val_re(genDelimitedKeyValPattern("&")), + headers_re(to_regexmap(sigsSource["headers_re"].get(), error)), + format_magic_binary_re(sigsSource["format_magic_binary_re"].get(), error, "format_magic_binary_re"), + params_type_re(to_regexmap(sigsSource["format_types_regex_list"].get(), error)), + resp_hdr_pattern_regex_list(to_strvec(sigsSource["resp_hdr_pattern_regex_list"].get()), + error, "resp_hdr_pattern_regex_list", nullptr), + resp_hdr_words_regex_list(to_strvec(sigsSource["resp_hdr_words_regex_list"].get()), + error, "resp_hdr_words_regex_list", nullptr), + resp_body_pattern_regex_list(to_strvec(sigsSource["resp_body_pattern_regex_list"].get()), + error, "resp_body_pattern_regex_list", nullptr), + resp_body_words_regex_list(to_strvec(sigsSource["resp_body_words_regex_list"].get()), + error, "resp_body_words_regex_list", nullptr), + remove_keywords_always( + to_strset(sigsSource["remove_keywords_always"].get())), + user_agent_prefix_re(sigsSource["user_agent_prefix_re"].get()), + binary_data_kw_filter(sigsSource["binary_data_kw_filter"].get()), + wbxml_data_kw_filter(sigsSource["wbxml_data_kw_filter"].get()) +{ + +} + +Signatures::~Signatures() +{ +} + +bool Signatures::fail() +{ + return error; +} + +picojson::value::object Signatures::loadSource(const std::string& sigsFname) +{ + picojson::value doc; + std::ifstream f(sigsFname.c_str()); + + if (f.fail()) { + dbgError(D_WAAP) << "Failed to open json data file '" << sigsFname << "'!"; + error = true; // flag an error + return picojson::value::object(); + } + + int length; + f.seekg(0, std::ios::end); // go to the end + length = f.tellg(); // report location (this is the length) + char* buffer = new char[length]; // allocate memory for a buffer of appropriate dimension + f.seekg(0, std::ios::beg); // go back to the beginning + f.read(buffer, length); // read the whole file into the buffer + f.close(); + + std::string dataObfuscated(buffer, length); + + delete[] buffer; + std::stringstream ss(dataObfuscated); + ss >> doc; + + if (!picojson::get_last_error().empty()) { + dbgError(D_WAAP) << "WaapAssetState::loadSource('" << sigsFname << "') failed (parse error: '" << + picojson::get_last_error() << "')."; + error = true; // flag an error + return picojson::value::object(); + } + + return doc.get(); +} diff --git a/components/security_apps/waap/waap_clib/Signatures.h b/components/security_apps/waap/waap_clib/Signatures.h new file mode 100755 index 0000000..302e7ce --- /dev/null +++ b/components/security_apps/waap/waap_clib/Signatures.h @@ -0,0 +1,93 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __SIGNATURES_H__ +#define __SIGNATURES_H__ + +#include "Waf2Regex.h" +#include "picojson.h" +#include + +class Signatures { +private: + // json parsed sources (not really needed once data is loaded) + picojson::value::object sigsSource; + bool error; +public: + Signatures(const std::string& filepath); + ~Signatures(); + + bool fail(); + + std::shared_ptr m_regexPreconditions; + + // Regexes loaded from compiled signatures + const Regex words_regex; + const Regex specific_acuracy_keywords_regex; + const Regex pattern_regex; + const Regex un_escape_pattern; + const Regex quotes_ev_pattern; + const Regex comment_ev_pattern; + const Regex quotes_space_ev_pattern; + const Regex allowed_text_re; + const Regex pipe_split_re; + const Regex semicolon_split_re; + const Regex longtext_re; + const Regex nospaces_long_value_re; + const Regex good_header_name_re; + const Regex good_header_value_re; + const std::set ignored_for_nospace_long_value; + const std::set global_ignored_keywords; + const std::set global_ignored_patterns; + const std::set url_ignored_keywords; + const std::set url_ignored_patterns; + const Regex url_ignored_re; + const std::set header_ignored_keywords; + const std::set header_ignored_patterns; + const Regex header_ignored_re; + const std::map> filter_parameters; + const std::map> m_attack_types; + const Regex php_serialize_identifier; + const Regex html_regex; + const Regex uri_parser_regex; + const boost::regex confluence_macro_re; + const boost::regex pipes_delimited_key_val_re; + const boost::regex semicolon_delimited_key_val_re; + const boost::regex asterisk_delimited_key_val_re; + const boost::regex comma_delimited_key_val_re; + const boost::regex ampersand_delimited_key_val_re; +#if 0 // Removed by Pavel's request. Leaving here in case he'll want to add this back... + const std::set cookie_ignored_keywords; + const std::set cookie_ignored_patterns; + const Regex cookie_ignored_re; +#endif + std::map headers_re; + const Regex format_magic_binary_re; + std::map params_type_re; + + // Signatures for responses + const Regex resp_hdr_pattern_regex_list; + const Regex resp_hdr_words_regex_list; + const Regex resp_body_pattern_regex_list; + const Regex resp_body_words_regex_list; + + const std::set remove_keywords_always; + const boost::regex user_agent_prefix_re; + const boost::regex binary_data_kw_filter; + const boost::regex wbxml_data_kw_filter; + +private: + picojson::value::object loadSource(const std::string& sigsFname); +}; + +#endif diff --git a/components/security_apps/waap/waap_clib/SingleDecision.cc b/components/security_apps/waap/waap_clib/SingleDecision.cc new file mode 100755 index 0000000..b7a38d1 --- /dev/null +++ b/components/security_apps/waap/waap_clib/SingleDecision.cc @@ -0,0 +1,53 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "SingleDecision.h" +#include "debug.h" + +USE_DEBUG_FLAG(D_WAAP); + +SingleDecision::SingleDecision(DecisionType type): + m_type(type), + m_log(false), + m_block(false) +{} + +SingleDecision::~SingleDecision() +{} + +DecisionType SingleDecision::getType() const +{ + return m_type; +} + +bool SingleDecision::shouldLog() const +{ + return m_log; +} + +bool SingleDecision::shouldBlock() const +{ + return m_block; +} + +void SingleDecision::setLog(bool log) +{ + dbgTrace(D_WAAP) << "Decision " << getTypeStr() << " changes should log from " << m_log << " to " << log; + m_log = log; +} + +void SingleDecision::setBlock(bool block) +{ + dbgTrace(D_WAAP) << "Decision " << getTypeStr() << " changes should block from " << m_block << " to " << block; + m_block = block; +} diff --git a/components/security_apps/waap/waap_clib/SingleDecision.h b/components/security_apps/waap/waap_clib/SingleDecision.h new file mode 100755 index 0000000..da39f33 --- /dev/null +++ b/components/security_apps/waap/waap_clib/SingleDecision.h @@ -0,0 +1,39 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __SINGLE_DECISION_H__ +#define __SINGLE_DECISION_H__ + +#include "DecisionType.h" +#include + +class SingleDecision +{ +public: + explicit SingleDecision(DecisionType type); + virtual ~SingleDecision(); + + void setLog(bool log); + void setBlock(bool block); + DecisionType getType() const; + bool shouldLog() const; + bool shouldBlock() const; + virtual std::string getTypeStr() const = 0; + +protected: + DecisionType m_type; + bool m_log; + bool m_block; +}; + +#endif diff --git a/components/security_apps/waap/waap_clib/SyncLearningNotification.cc b/components/security_apps/waap/waap_clib/SyncLearningNotification.cc new file mode 100755 index 0000000..6f794f6 --- /dev/null +++ b/components/security_apps/waap/waap_clib/SyncLearningNotification.cc @@ -0,0 +1,58 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "SyncLearningNotification.h" + +SyncLearningNotificationObject::SyncLearningNotificationObject(const std::string& asset_id, + const std::string& type, + const std::string& window_id) : + m_asset_id(asset_id), + m_type(type), + m_window_id(window_id) +{ + +} + +SyncLearningNotificationObject::~SyncLearningNotificationObject() +{ + +} + +void SyncLearningNotificationObject::serialize(cereal::JSONOutputArchive& ar) const +{ + ar.setNextName("notificationConsumerData"); + ar.startNode(); + ar.setNextName("syncLearnNotificationConsumers"); + ar.startNode(); + ar(cereal::make_nvp("assetId", m_asset_id)); + ar(cereal::make_nvp("type", m_type)); + ar(cereal::make_nvp("windowId", m_window_id)); + ar.finishNode(); + ar.finishNode(); +} + +std::string SyncLearningNotificationObject::toString() const +{ + std::stringstream ss; + { + cereal::JSONOutputArchive ar(ss); + serialize(ar); + } + + return ss.str(); +} + +std::ostream& operator<<(std::ostream& os, const SyncLearningNotificationObject& obj) +{ + return os << obj.toString(); +} diff --git a/components/security_apps/waap/waap_clib/SyncLearningNotification.h b/components/security_apps/waap/waap_clib/SyncLearningNotification.h new file mode 100755 index 0000000..2dd92b1 --- /dev/null +++ b/components/security_apps/waap/waap_clib/SyncLearningNotification.h @@ -0,0 +1,59 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __SYNC_LEARNING_NOTIFICATION_OBJECT_H__ +#define __SYNC_LEARNING_NOTIFICATION_OBJECT_H__ + +#include +#include +#include "cereal/archives/json.hpp" +#include "report/report.h" +#include "rest.h" + +class SyncLearningNotificationObject +{ +public: + explicit SyncLearningNotificationObject( + const std::string& asset_id, + const std::string& type, + const std::string& window_id + ); + ~SyncLearningNotificationObject(); + void serialize(cereal::JSONOutputArchive& ar) const; + + friend std::ostream& operator<<(std::ostream& os, const SyncLearningNotificationObject& obj); + +private: + std::string toString() const; + + std::string m_asset_id; + std::string m_type; + std::string m_window_id; +}; + +class SyncLearningObject : public ClientRest +{ +public: + SyncLearningObject( + const std::string& _asset_id, + const std::string& _type, + const std::string& _window_id + ) : assetId(_asset_id), type(_type), windowId(_window_id) {} + +private: + C2S_PARAM(std::string, assetId); + C2S_PARAM(std::string, type); + C2S_PARAM(std::string, windowId); +}; + +#endif diff --git a/components/security_apps/waap/waap_clib/Telemetry.cc b/components/security_apps/waap/waap_clib/Telemetry.cc new file mode 100755 index 0000000..2c344d6 --- /dev/null +++ b/components/security_apps/waap/waap_clib/Telemetry.cc @@ -0,0 +1,304 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "telemetry.h" +#include "waap.h" +#include "report/report.h" +#include "log_generator.h" +#include "generic_rulebase/triggers_config.h" +#include "config.h" +#include "maybe_res.h" +#include "LogGenWrapper.h" +#include + +USE_DEBUG_FLAG(D_WAAP); + +#define LOGGING_INTERVAL_IN_MINUTES 10 + +using namespace std; + +void +WaapTelemetrics::initMetrics() +{ + requests.report(0); + sources.report(0); + threat_info.report(0); + threat_low.report(0); + threat_medium.report(0); + threat_high.report(0); + api_blocked.report(0); + bot_blocked.report(0); + waf_blocked.report(0); + force_and_block_exceptions.report(0); +} +void +WaapTelemetrics::updateMetrics(const string &asset_id, const DecisionTelemetryData &data) +{ + initMetrics(); + requests.report(1); + if (sources_seen.find(data.source) == sources_seen.end()) { + if (sources.getCounter() == 0) sources_seen.clear(); + sources_seen.insert(data.source); + sources.report(1); + } + + if (data.blockType == WAF_BLOCK || data.blockType == NOT_BLOCKING) + { + switch (data.threat) + { + case NO_THREAT: { + break; + } + case THREAT_INFO: { + threat_info.report(1); + break; + } + case LOW_THREAT: { + threat_low.report(1); + break; + } + case MEDIUM_THREAT: { + threat_medium.report(1); + break; + } + case HIGH_THREAT: { + threat_high.report(1); + break; + } + default: { + dbgWarning(D_WAAP) << "Unexpected Enum value: " << data.threat; + break; + } + } + } + + switch (data.blockType) + { + case API_BLOCK: { + api_blocked.report(1); + break; + } + case BOT_BLOCK: { + bot_blocked.report(1); + break; + } + case WAF_BLOCK: { + waf_blocked.report(1); + break; + } + case FORCE_BLOCK: + case FORCE_EXCEPTION: { + force_and_block_exceptions.report(1); + break; + } + case NOT_BLOCKING: { + break; + } + default: { + dbgWarning(D_WAAP) << "Unexpected Enum value: " << data.blockType; + break; + } + } +} + +void +WaapAttackTypesMetrics::initMetrics() +{ + sql_inj.report(0); + vulnerability_scan.report(0); + path_traversal.report(0); + ldap_inj.report(0); + evasion_techs.report(0); + remote_code_exec.report(0); + xml_extern_entity.report(0); + cross_site_scripting.report(0); + general.report(0); +} + +void +WaapAttackTypesMetrics::updateMetrics(const string &asset_id, const DecisionTelemetryData &data) +{ + if (data.blockType == FORCE_EXCEPTION) { + dbgInfo(D_WAAP) << "Data block type is FORCE_EXCEPTION, no update needed"; + return; + } + + if (!data.attackTypes.empty()) initMetrics(); + + for (const auto &attackType : data.attackTypes) { + if (attackType == "SQL Injection") sql_inj.report(1); + if (attackType == "Vulnerability Scanning") vulnerability_scan.report(1); + if (attackType == "Path Traversal") path_traversal.report(1); + if (attackType == "LDAP Injection") ldap_inj.report(1); + if (attackType == "Evasion Techniques") evasion_techs.report(1); + if (attackType == "Remote Code Execution") remote_code_exec.report(1); + if (attackType == "XML External Entity") xml_extern_entity.report(1); + if (attackType == "Cross Site Scripting") cross_site_scripting.report(1); + if (attackType == "General") general.report(1); + } +} + +void +WaapMetricWrapper::upon(const WaapTelemetryEvent &event) +{ + const string &asset_id = event.getAssetId(); + const DecisionTelemetryData &data = event.getData(); + + dbgTrace(D_WAAP) + << "Log the decision for telemetry. Asset ID: " + << asset_id + << ", Practice ID: " + << data.practiceId + << ", Source: " + << data.source + << ", Block type: " + << data.blockType + << ", Threat level: " + << data.threat; + + if (!telemetries.count(asset_id)) { + telemetries.emplace(asset_id, make_shared()); + telemetries[asset_id]->init( + "WAAP telemetry", + ReportIS::AudienceTeam::WAAP, + ReportIS::IssuingEngine::AGENT_CORE, + chrono::minutes(10), + true, + ReportIS::Audience::SECURITY + ); + + telemetries[asset_id]->registerContext( + "pracitceType", + string("Threat Prevention"), + EnvKeyAttr::LogSection::SOURCE + ); + telemetries[asset_id]->registerContext( + "practiceSubType", + string("Web Application"), + EnvKeyAttr::LogSection::SOURCE + ); + telemetries[asset_id]->registerContext("assetId", asset_id, EnvKeyAttr::LogSection::SOURCE); + telemetries[asset_id]->registerContext("assetName", data.assetName, EnvKeyAttr::LogSection::SOURCE); + telemetries[asset_id]->registerContext("practiceId", data.practiceId, EnvKeyAttr::LogSection::SOURCE); + telemetries[asset_id]->registerContext( + "practiceName", + data.practiceName, + EnvKeyAttr::LogSection::SOURCE + ); + + telemetries[asset_id]->registerListener(); + } + if (!attack_types_telemetries.count(asset_id)) { + attack_types_telemetries.emplace(asset_id, make_shared()); + attack_types_telemetries[asset_id]->init( + "WAAP attack type telemetry", + ReportIS::AudienceTeam::WAAP, + ReportIS::IssuingEngine::AGENT_CORE, + chrono::minutes(10), + true, + ReportIS::Audience::SECURITY + ); + + attack_types_telemetries[asset_id]->registerContext( + "pracitceType", + string("Threat Prevention"), + EnvKeyAttr::LogSection::SOURCE + ); + attack_types_telemetries[asset_id]->registerContext( + "practiceSubType", + string("Web Application"), + EnvKeyAttr::LogSection::SOURCE + ); + attack_types_telemetries[asset_id]->registerContext( + "assetId", + asset_id, + EnvKeyAttr::LogSection::SOURCE + ); + attack_types_telemetries[asset_id]->registerContext( + "assetName", + data.assetName, + EnvKeyAttr::LogSection::SOURCE + ); + attack_types_telemetries[asset_id]->registerContext( + "practiceId", + data.practiceId, + EnvKeyAttr::LogSection::SOURCE + ); + attack_types_telemetries[asset_id]->registerContext( + "practiceName", + data.practiceName, + EnvKeyAttr::LogSection::SOURCE + ); + + attack_types_telemetries[asset_id]->registerListener(); + } + + telemetries[asset_id]->updateMetrics(asset_id, data); + attack_types_telemetries[asset_id]->updateMetrics(asset_id, data); + + auto agent_mode = Singleton::Consume::by()->getOrchestrationMode(); + string tenant_id = Singleton::Consume::by()->getTenantId(); + if (agent_mode == OrchestrationMode::HYBRID || tenant_id.rfind("org_", 0) == 0) { + if (!metrics.count(asset_id)) { + metrics.emplace(asset_id, make_shared()); + metrics[asset_id]->init( + "Waap Metrics", + ReportIS::AudienceTeam::WAAP, + ReportIS::IssuingEngine::AGENT_CORE, + chrono::minutes(10), + true, + ReportIS::Audience::INTERNAL + ); + metrics[asset_id]->registerListener(); + } + if (!attack_types.count(asset_id)) { + attack_types.emplace(asset_id, make_shared()); + attack_types[asset_id]->init( + "WAAP Attack Type Metrics", + ReportIS::AudienceTeam::WAAP, + ReportIS::IssuingEngine::AGENT_CORE, + chrono::minutes(10), + true, + ReportIS::Audience::INTERNAL + ); + attack_types[asset_id]->registerListener(); + } + + metrics[asset_id]->updateMetrics(asset_id, data); + attack_types[asset_id]->updateMetrics(asset_id, data); + } +} + +void +AssetsMetric::upon(const AssetCountEvent &event) +{ + int assets_count = event.getAssetCount(); + + switch (event.getAssetType()) { + case AssetType::API: { + api_assets.report(assets_count); + break; + } + case AssetType::WEB: { + web_assets.report(assets_count); + break; + } + case AssetType::ALL: { + all_assets.report(assets_count); + break; + } + default: { + dbgWarning(D_WAAP) << "Invalid Asset Type was reported"; + } + } +} diff --git a/components/security_apps/waap/waap_clib/TrustedSources.cc b/components/security_apps/waap/waap_clib/TrustedSources.cc new file mode 100755 index 0000000..aa11c07 --- /dev/null +++ b/components/security_apps/waap/waap_clib/TrustedSources.cc @@ -0,0 +1,217 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include + +#include "TrustedSources.h" +#include "Waf2Util.h" +#include "CidrMatch.h" +#include "agent_core_utilities.h" + +using namespace Waap::TrustedSources; + +TrustedSourcesParameter::TrustedSourcesParameter() : m_identifiers() +{ + +} + +bool TrustedSourcesParameter::isSourceTrusted(std::string source, TrustedSourceType srcType) +{ + if (m_identifiers.empty()) + { + return false; + } + + if (source.empty()) + { + return false; + } + switch (srcType) + { + case SOURCE_IP: + case X_FORWARDED_FOR: + return m_identifiers[0].isCidrMatch(source, srcType); + case COOKIE_OAUTH2_PROXY: + return m_identifiers[0].isRegexMatch(source, COOKIE_OAUTH2_PROXY); + case SM_USER: + return m_identifiers[0].isRegexMatch(source, SM_USER); + case UNKNOWN: + break; + default: + break; + } + return false; +} + +size_t TrustedSourcesParameter::getNumOfSources() +{ + if (m_identifiers.empty()) + { + return (size_t)(-1); + } + return m_identifiers[0].getNumOfSources(); +} + +std::set Waap::TrustedSources::TrustedSourcesParameter::getTrustedTypes() +{ + if (m_identifiers.empty()) + { + return std::set(); + } + return m_identifiers[0].getTrustedTypes(); +} + + +bool SourcesIdentifers::isCidrMatch(const std::string &source, const TrustedSourceType &trustedSourceType) const +{ + auto found = m_identifiersMap.find(trustedSourceType); + if (found == m_identifiersMap.end()) + { + return false; + } + const std::vector& cidrs = found->second; + for (auto cidr : cidrs) + { + if (Waap::Util::cidrMatch(source, cidr)) + { + dbgTrace(D_WAAP) << "source: " << source << " is trusted for type: " << trustedSourceType << + ", cidr: " << cidr; + return true; + } + } + return false; +} + +bool SourcesIdentifers::isRegexMatch(const std::string &source, const TrustedSourceType& type) const +{ + auto found = m_identifiersMap.find(type); + if (found == m_identifiersMap.end()) + { + return false; + } + const std::vector& regexes = found->second; + for (auto regex : regexes) + { + boost::regex expr{ regex }; + boost::smatch matches; + if (NGEN::Regex::regexSearch(__FILE__, __LINE__, source, matches, expr)) + { + dbgTrace(D_WAAP) << "source: " << source << " is trusted for type: " << type << + ", expr: " << regex; + return true; + } + } + return false; +} + +size_t SourcesIdentifers::getNumOfSources() const +{ + return m_minSources; +} + +const std::set& SourcesIdentifers::getTrustedTypes() +{ + return m_trustedTypes; +} + + +bool SourcesIdentifers::operator!=(const SourcesIdentifers& other) const +{ + if (m_identifiersMap.size() != other.m_identifiersMap.size()) + { + return true; + } + if (m_minSources != other.m_minSources) + { + return true; + } + + for (auto identifier : m_identifiersMap) + { + if (other.m_identifiersMap.find(identifier.first) == other.m_identifiersMap.end()) + { + return true; + } + TrustedSourceType currType = identifier.first; + const std::vector& values = identifier.second; + std::vector otherValues = other.m_identifiersMap.at(currType); + if (values.size() != otherValues.size()) + { + return true; + } + for (size_t i = 0; i < values.size(); i++) + { + if (values[i] != otherValues[i]) + { + return true; + } + } + } + + return false; +} + + +Identifer::Identifer() : identitySource(UNKNOWN), value() +{ +} + +TrustedSourceType Identifer::convertSourceIdentifierToEnum(std::string identifierType) +{ + static const std::string SourceIp = "Source IP"; + static const std::string cookie = "Cookie:_oauth2_proxy"; + static const std::string smUser = "Header:sm_user"; + static const std::string forwrded = "X-Forwarded-For"; + if (memcaseinsensitivecmp(identifierType.c_str(), identifierType.size(), SourceIp.c_str(), SourceIp.size())) + { + return SOURCE_IP; + } + if (memcaseinsensitivecmp(identifierType.c_str(), identifierType.size(), cookie.c_str(), cookie.size())) + { + return COOKIE_OAUTH2_PROXY; + } + if (memcaseinsensitivecmp(identifierType.c_str(), identifierType.size(), forwrded.c_str(), forwrded.size())) + { + return X_FORWARDED_FOR; + } + if (memcaseinsensitivecmp(identifierType.c_str(), identifierType.size(), smUser.c_str(), smUser.size())) + { + return SM_USER; + } + dbgTrace(D_WAAP) << identifierType << " is not a recognized identifier type"; + return UNKNOWN; +} + +bool TrustedSourcesParameter::operator==(const TrustedSourcesParameter &other) const +{ + return !(*this != other); +} + +bool TrustedSourcesParameter::operator!=(const TrustedSourcesParameter& other) const +{ + if (m_identifiers.size() != other.m_identifiers.size()) + { + return true; + } + + for (size_t i = 0; i < m_identifiers.size(); i++) + { + if (m_identifiers[i] != other.m_identifiers[i]) + { + return true; + } + } + + return false; +} diff --git a/components/security_apps/waap/waap_clib/TrustedSources.h b/components/security_apps/waap/waap_clib/TrustedSources.h new file mode 100755 index 0000000..20f4c5e --- /dev/null +++ b/components/security_apps/waap/waap_clib/TrustedSources.h @@ -0,0 +1,111 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include +#include +#include +#include "debug.h" + +USE_DEBUG_FLAG(D_WAAP); + +// used to load trusted sources policy +namespace Waap { + namespace TrustedSources { + + enum TrustedSourceType { + UNKNOWN, + SOURCE_IP, + X_FORWARDED_FOR, + COOKIE_OAUTH2_PROXY, + SM_USER + }; + + class Identifer + { + public: + Identifer(); + + template + void serialize(_A& ar) { + std::string temp; + ar(cereal::make_nvp("sourceIdentifier", temp), + cereal::make_nvp("value", value)); + identitySource = convertSourceIdentifierToEnum(temp); + if (identitySource == UNKNOWN) + { + dbgDebug(D_WAAP) << "loaded " << temp << " from policy is not a recognized source identifier"; + } + } + + static TrustedSourceType convertSourceIdentifierToEnum(std::string identifierType); + + TrustedSourceType identitySource; + std::string value; + }; + + class SourcesIdentifers + { + public: + template + void serialize(_A& ar) { + std::vector identifiers; + ar(cereal::make_nvp("sourcesIdentifiers", identifiers), + cereal::make_nvp("numOfSources", m_minSources)); + for (auto identifier : identifiers) + { + if (identifier.identitySource != UNKNOWN) + { + m_identifiersMap[identifier.identitySource].push_back(identifier.value); + m_trustedTypes.insert(identifier.identitySource); + } + } + } + + bool isCidrMatch(const std::string &source, const TrustedSourceType &type) const; + bool isRegexMatch(const std::string &source, const TrustedSourceType& type) const; + size_t getNumOfSources() const; + const std::set& getTrustedTypes(); + + inline bool operator!=(const SourcesIdentifers& other) const; + private: + std::map> m_identifiersMap; + std::set m_trustedTypes; + size_t m_minSources; + }; + + class TrustedSourcesParameter + { + public: + template + TrustedSourcesParameter(_A& ar) { + ar(cereal::make_nvp("trustedSources", m_identifiers)); + } + + TrustedSourcesParameter(); + + template + void serialize(Archive& ar) { + ar(cereal::make_nvp("trustedSources", m_identifiers)); + } + bool isSourceTrusted(std::string source, TrustedSourceType srcType); + size_t getNumOfSources(); + std::set getTrustedTypes(); + bool operator==(const TrustedSourcesParameter &other) const; + bool operator!=(const TrustedSourcesParameter& other) const; + private: + std::vector m_identifiers; + }; + } +} diff --git a/components/security_apps/waap/waap_clib/TrustedSourcesConfidence.cc b/components/security_apps/waap/waap_clib/TrustedSourcesConfidence.cc new file mode 100755 index 0000000..b59c8a2 --- /dev/null +++ b/components/security_apps/waap/waap_clib/TrustedSourcesConfidence.cc @@ -0,0 +1,269 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "TrustedSourcesConfidence.h" +#include "i_messaging.h" +#include "waap.h" +#include "Waf2Util.h" + +USE_DEBUG_FLAG(D_WAAP_CONFIDENCE_CALCULATOR); +#define SYNC_WAIT_TIME std::chrono::seconds(300) // 5 minutes in seconds + +TrustedSourcesConfidenceCalculator::TrustedSourcesConfidenceCalculator( + std::string path, + const std::string& remotePath, + const std::string& assetId) + : + SerializeToLocalAndRemoteSyncBase(std::chrono::minutes(120), + SYNC_WAIT_TIME, + path, + (remotePath == "") ? remotePath : remotePath + "/Trust", + assetId, + "TrustedSourcesConfidenceCalculator") +{ + restore(); +} + +bool TrustedSourcesConfidenceCalculator::is_confident(Key key, Val value, size_t minSources) const +{ + auto sourceCtrItr = m_logger.find(key); + if (sourceCtrItr != m_logger.end()) + { + auto sourceSetItr = sourceCtrItr->second.find(value); + if (sourceSetItr != sourceCtrItr->second.end()) + { + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "The number of trusted sources for " << key + << " : " << value << " is " << sourceSetItr->second.size(); + return sourceSetItr->second.size() >= minSources; + } + else + { + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "Failed to find the value(" << value << ")"; + } + } + else + { + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "Failed to find the key(" << key << ")"; + } + return false; +} + + +class GetTrustedFile : public RestGetFile +{ +public: + GetTrustedFile() + { + } + + Maybe + getTrustedLogs() const + { + if (!logger.get().empty()) return logger.get(); + return genError("failed to get file"); + } + +private: + S2C_PARAM(TrustedSourcesConfidenceCalculator::KeyValSourceLogger, logger) +}; + +class TrsutedSourcesLogger : public RestGetFile +{ +public: + TrsutedSourcesLogger(const TrustedSourcesConfidenceCalculator::KeyValSourceLogger& _logger) + : logger(_logger) + { + + } +private: + C2S_PARAM(TrustedSourcesConfidenceCalculator::KeyValSourceLogger, logger); +}; + +bool TrustedSourcesConfidenceCalculator::postData() +{ + std::string url = getPostDataUrl(); + + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "Sending the data to: " << url; + + TrsutedSourcesLogger logger(m_logger); + return sendNoReplyObjectWithRetry(logger, + I_Messaging::Method::PUT, + url); +} + +void TrustedSourcesConfidenceCalculator::pullData(const std::vector& files) +{ + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "Fetching the window data for trusted sources"; + std::string url = getPostDataUrl(); + std::string sentFile = url.erase(0, url.find_first_of('/') + 1); + for (auto file : files) + { + if (file == sentFile) + { + continue; + } + GetTrustedFile getTrustFile; + bool res = sendObjectWithRetry(getTrustFile, + I_Messaging::Method::GET, + getUri() + "/" + file); + if (res && getTrustFile.getTrustedLogs().ok()) + { + mergeFromRemote(getTrustFile.getTrustedLogs().unpack()); + } + } +} + +void TrustedSourcesConfidenceCalculator::processData() +{ + +} + +void TrustedSourcesConfidenceCalculator::updateState(const std::vector& files) +{ + m_logger.clear(); + pullProcessedData(files); +} + +void TrustedSourcesConfidenceCalculator::pullProcessedData(const std::vector& files) +{ + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "Fetching the logger object for trusted sources"; + for (auto file : files) + { + GetTrustedFile getTrustFile; + bool res = sendObjectWithRetry(getTrustFile, + I_Messaging::Method::GET, + getUri() + "/" + file); + if (res && getTrustFile.getTrustedLogs().ok()) + { + mergeFromRemote(getTrustFile.getTrustedLogs().unpack()); + } + } +} + +void TrustedSourcesConfidenceCalculator::postProcessedData() +{ + std::string url = getUri() + "/" + m_remotePath + "/processed/data.data"; + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "Sending the processed data to: " << url; + + TrsutedSourcesLogger logger(m_logger); + sendNoReplyObjectWithRetry(logger, + I_Messaging::Method::PUT, + url); +} + +TrustedSourcesConfidenceCalculator::ValuesSet TrustedSourcesConfidenceCalculator::getConfidenceValues( + const Key& key, + size_t minSources) const +{ + ValuesSet values; + auto sourceCtrItr = m_logger.find(key); + if (sourceCtrItr != m_logger.end()) + { + for (auto sourceSetItr : sourceCtrItr->second) + { + if (sourceSetItr.second.size() >= minSources) + { + values.insert(sourceSetItr.first); + } + } + } + else + { + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "Failed to find the key(" << key << ")"; + } + return values; +} + +void TrustedSourcesConfidenceCalculator::serialize(std::ostream& stream) +{ + cereal::JSONOutputArchive archive(stream); + + archive(cereal::make_nvp("version", 2), cereal::make_nvp("logger", m_logger)); +} + +void TrustedSourcesConfidenceCalculator::deserialize(std::istream& stream) +{ + cereal::JSONInputArchive archive(stream); + size_t version = 0; + + try + { + archive(cereal::make_nvp("version", version)); + } + catch (std::runtime_error & e) { + archive.setNextName(nullptr); + version = 0; + dbgDebug(D_WAAP) << "Can't load file version: " << e.what(); + } + + switch (version) + { + case 2: + { + archive(cereal::make_nvp("logger", m_logger)); + break; + } + case 1: + { + KeyValSourceLogger logger; + archive(cereal::make_nvp("logger", logger)); + for (auto& log : logger) + { + m_logger[normalize_param(log.first)] = log.second; + } + break; + } + case 0: + { + archive(cereal::make_nvp("m_logger", m_logger)); + break; + } + default: + dbgError(D_WAAP) << "unknown file format version: " << version; + break; + } +} + +void TrustedSourcesConfidenceCalculator::mergeFromRemote(const KeyValSourceLogger& logs) +{ + for (auto& srcCounterItr : logs) + { + for (auto& sourcesItr : srcCounterItr.second) + { + for (auto& src : sourcesItr.second) + { + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) << "Registering the source: " << src + << " for the value: " << sourcesItr.first << " and the key: " << srcCounterItr.first; + m_logger[normalize_param(srcCounterItr.first)][sourcesItr.first].insert(src); + } + } + } +} + +void TrustedSourcesConfidenceCalculator::log(Key key, Val value, Source source) +{ + dbgTrace(D_WAAP_CONFIDENCE_CALCULATOR) + << "Logging the value: " + << value + << " for the key: " + << key + << " from the source: " + << source; + m_logger[key][value].insert(source); + saveData(); +} + +void TrustedSourcesConfidenceCalculator::reset() +{ + m_logger.clear(); +} diff --git a/components/security_apps/waap/waap_clib/TrustedSourcesConfidence.h b/components/security_apps/waap/waap_clib/TrustedSourcesConfidence.h new file mode 100755 index 0000000..db31bb8 --- /dev/null +++ b/components/security_apps/waap/waap_clib/TrustedSourcesConfidence.h @@ -0,0 +1,57 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include "i_serialize.h" +#include +#include +#include + +USE_DEBUG_FLAG(D_WAAP); + +// this class is responsible for logging trusted sources indicators matches (without validation) +class TrustedSourcesConfidenceCalculator : public SerializeToLocalAndRemoteSyncBase +{ +public: + typedef std::string Key; + typedef std::string Val; + typedef std::string Source; + typedef std::set ValuesSet; + typedef std::unordered_set SourcesSet; + typedef std::unordered_map SourcesCounter; + typedef std::unordered_map KeyValSourceLogger; + + TrustedSourcesConfidenceCalculator(std::string path, const std::string& remotePath, + const std::string& assetId); + bool is_confident(Key key, Val value, size_t minSources) const; + + virtual bool postData(); + virtual void pullData(const std::vector& files); + virtual void processData(); + virtual void postProcessedData(); + virtual void pullProcessedData(const std::vector& files); + virtual void updateState(const std::vector& files); + + ValuesSet getConfidenceValues(const Key& key, size_t minSources) const; + + virtual void serialize(std::ostream& stream); + virtual void deserialize(std::istream& stream); + + void mergeFromRemote(const KeyValSourceLogger& logs); + + void log(Key key, Val value, Source source); + void reset(); + +private: + KeyValSourceLogger m_logger; +}; diff --git a/components/security_apps/waap/waap_clib/TuningDecision.cc b/components/security_apps/waap/waap_clib/TuningDecision.cc new file mode 100755 index 0000000..e5f5d3d --- /dev/null +++ b/components/security_apps/waap/waap_clib/TuningDecision.cc @@ -0,0 +1,158 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "TuningDecisions.h" +#include "i_mainloop.h" +#include "i_serialize.h" +#include "waap.h" + +static const std::string BASE_URI = "/storage/waap/"; +USE_DEBUG_FLAG(D_WAAP); + +TuningDecision::TuningDecision(const std::string& remotePath) : + m_remotePath(remotePath + "/tuning") +{ + if (remotePath == "") + { + return; + } + Singleton::Consume::by()->addRecurringRoutine( + I_MainLoop::RoutineType::System, + std::chrono::minutes(10), + [&]() { updateDecisions(); }, + "Get tuning updates" + ); +} + +TuningDecision::~TuningDecision() +{ + +} + +struct TuningEvent +{ + template + void serialize(Archive& ar) + { + ar(cereal::make_nvp("decision", decision)); + ar(cereal::make_nvp("eventType", eventType)); + ar(cereal::make_nvp("eventTitle", eventTitle)); + } + std::string decision; + std::string eventType; + std::string eventTitle; +}; + +class TuningEvents : public RestGetFile +{ +public: + TuningEvents() + { + + } + + Maybe> getTuningEvents() + { + return decisions.get(); + } + +private: + S2C_PARAM(std::vector, decisions); +}; + +TuningDecisionEnum TuningDecision::convertDecision(std::string decisionStr) +{ + if (decisionStr == "benign") + { + return BENIGN; + } + if (decisionStr == "malicious") + { + return MALICIOUS; + } + if (decisionStr == "dismiss") + { + return DISMISS; + } + return NO_DECISION; +} + +TuningDecisionType TuningDecision::convertDecisionType(std::string decisionTypeStr) +{ + if (decisionTypeStr == "source") + { + return TuningDecisionType::SOURCE; + } + if (decisionTypeStr == "url") + { + return TuningDecisionType::URL; + } + if (decisionTypeStr == "parameterName") + { + return TuningDecisionType::PARAM_NAME; + } + if (decisionTypeStr == "parameterValue") + { + return TuningDecisionType::PARAM_VALUE; + } + return TuningDecisionType::UNKNOWN; +} + +void TuningDecision::updateDecisions() +{ + TuningEvents tuningEvents; + RemoteFilesList tuningDecisionFiles; + bool isSuccessful = sendObject(tuningDecisionFiles, + I_Messaging::Method::GET, + BASE_URI + "?list-type=2&prefix=" + m_remotePath); + + if (!isSuccessful || tuningDecisionFiles.getFilesList().empty()) + { + dbgDebug(D_WAAP) << "Failed to get the list of files"; + return; + } + + if (!sendObject(tuningEvents, + I_Messaging::Method::GET, + BASE_URI + tuningDecisionFiles.getFilesList()[0])) + { + return; + } + m_decisions.clear(); + Maybe> events = tuningEvents.getTuningEvents(); + if (!events.ok()) + { + dbgDebug(D_WAAP) << "failed to parse events"; + return; + } + for (const auto& tEvent : events.unpack()) + { + TuningDecisionType type = convertDecisionType(tEvent.eventType); + m_decisions[type][tEvent.eventTitle] = convertDecision(tEvent.decision); + } +} + +TuningDecisionEnum TuningDecision::getDecision(std::string tuningValue, TuningDecisionType tuningType) +{ + const auto& typeDecisionsItr = m_decisions.find(tuningType); + if (typeDecisionsItr == m_decisions.cend()) + { + return NO_DECISION; + } + const auto& decisionItr = typeDecisionsItr->second.find(tuningValue); + if (decisionItr == typeDecisionsItr->second.cend()) + { + return NO_DECISION; + } + return decisionItr->second; +} diff --git a/components/security_apps/waap/waap_clib/TuningDecisions.h b/components/security_apps/waap/waap_clib/TuningDecisions.h new file mode 100755 index 0000000..62f17da --- /dev/null +++ b/components/security_apps/waap/waap_clib/TuningDecisions.h @@ -0,0 +1,89 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __TUNING_DECISIONS_H__ +#define __TUNING_DECISIONS_H__ + +#include +#include +#include "i_messaging.h" +#include "i_agent_details.h" +#include "waap.h" + +enum TuningDecisionEnum +{ + NO_DECISION, + DISMISS = NO_DECISION, + BENIGN, + MALICIOUS +}; + +enum TuningDecisionType +{ + UNKNOWN, + SOURCE, + URL, + PARAM_NAME, + PARAM_VALUE +}; + + +class TuningDecision +{ +public: + TuningDecision(const std::string& remotePath); + ~TuningDecision(); + + TuningDecisionEnum getDecision(std::string tuningValue, TuningDecisionType tuningType); +private: + void updateDecisions(); + TuningDecisionType convertDecisionType(std::string decisionTypeStr); + TuningDecisionEnum convertDecision(std::string decisionStr); + + + template + bool sendObject(T &obj, I_Messaging::Method method, std::string uri) + { + I_Messaging *messaging = Singleton::Consume::by(); + I_AgentDetails *agentDetails = Singleton::Consume::by(); + if (agentDetails->getOrchestrationMode() != OrchestrationMode::ONLINE) { + Flags conn_flags; + conn_flags.setFlag(MessageConnConfig::EXTERNAL); + std::string tenant_header = "X-Tenant-Id: " + agentDetails->getTenantId(); + + return messaging->sendObject( + obj, + method, + "fog-msrv-appsec-shared-files-svc", + 80, + conn_flags, + uri, + tenant_header, + nullptr, + MessageTypeTag::WAAP_LEARNING); + } + return messaging->sendObject( + obj, + method, + uri, + "", + nullptr, + true, + MessageTypeTag::WAAP_LEARNING); + } + + std::string m_remotePath; + std::map> m_decisions; +}; + +#endif diff --git a/components/security_apps/waap/waap_clib/TypeIndicatorsFilter.cc b/components/security_apps/waap/waap_clib/TypeIndicatorsFilter.cc new file mode 100755 index 0000000..f68c3e8 --- /dev/null +++ b/components/security_apps/waap/waap_clib/TypeIndicatorsFilter.cc @@ -0,0 +1,152 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "TypeIndicatorsFilter.h" +#include "waap.h" +#include "debug.h" +#include +#include +#include +#include "FpMitigation.h" +#include "i_transaction.h" +#include "Waf2Util.h" +#include "IndicatorsFiltersManager.h" +#include + +USE_DEBUG_FLAG(D_WAAP); + +#define TYPES_FILTER_PATH(dirPath) dirPath + "/4.data" +#define TYPES_FILTER_TRUST_PATH(dirPath) dirPath + "/9.data" + +TypeIndicatorFilter::TypeIndicatorFilter(I_WaapAssetState* pWaapAssetState, + const std::string& remotePath, + const std::string& assetId, + TuningDecision* tuning, + size_t minSources, + size_t minIntervals, + std::chrono::minutes intervalDuration, + double ratioThreshold) : + IndicatorFilterBase(TYPES_FILTER_PATH(pWaapAssetState->getSignaturesFilterDir()), + TYPES_FILTER_TRUST_PATH(pWaapAssetState->getSignaturesFilterDir()), + (remotePath == "") ? remotePath : remotePath + "/Type", + assetId, + minSources, + minIntervals, + intervalDuration, + ratioThreshold, + "unknown", + tuning), + m_pWaapAssetState(pWaapAssetState) +{ + m_confidence_calc.setOwner("TypeIndicatorFilter"); +} + +TypeIndicatorFilter::~TypeIndicatorFilter() +{ + +} + +bool TypeIndicatorFilter::shouldFilterKeyword(const std::string &key, const std::string &keyword) const +{ + auto keyTypes = getParamTypes(key); + std::string htmlParam = ".html"; + bool isHtmlInput = keyTypes.find("html_input") != keyTypes.end() || + (key.size() > htmlParam.size() && + key.compare(key.size() - htmlParam.size(), htmlParam.size(), htmlParam) == 0); + for (auto keyType : keyTypes) + { + if (keyType == "free_text" && !isHtmlInput) + { + return true; + } + if (m_pWaapAssetState->isKeywordOfType(keyword, Waap::Util::convertTypeStrToEnum(keyType))) + { + return true; + } + } + return false; +} + +void TypeIndicatorFilter::registerKeywords(const std::string& key, Waap::Keywords::KeywordsSet& keywords, + IWaf2Transaction* pTransaction) +{ + (void)keywords; + std::string sample = pTransaction->getLastScanSample(); + registerKeywords(key, sample, pTransaction); +} + +void TypeIndicatorFilter::registerKeywords(const std::string& key, const std::string& sample, + IWaf2Transaction* pTransaction) +{ + std::set types = m_pWaapAssetState->getSampleType(sample); + std::string source = pTransaction->getSourceIdentifier(); + std::string trusted_source = getTrustedSource(pTransaction); + + for (const std::string &type : types) + { + if (type == "local_file_path") + { + std::string location = IndicatorsFiltersManager::getLocationFromKey(key, pTransaction); + if (location == "url" || location == "referer") + { + continue; + } + } + registerKeyword(key, type, source, trusted_source); + if (m_tuning != nullptr && m_tuning->getDecision(pTransaction->getUri(), URL) == BENIGN) + { + source = "TuningDecisionSource_" + source; + registerKeyword(key, type, source, trusted_source); + } + + } +} + +void TypeIndicatorFilter::loadParams(std::shared_ptr pParams) +{ + ConfidenceCalculatorParams params; + + params.minSources = std::stoul( + pParams->getParamVal("typeIndicators.minSources", std::to_string(TYPE_FILTER_CONFIDENCE_MIN_SOURCES))); + params.minIntervals = std::stoul( + pParams->getParamVal("typeIndicators.minIntervals", std::to_string(TYPE_FILTER_CONFIDENCE_MIN_INTERVALS))); + params.intervalDuration = std::chrono::minutes(std::stoul( + pParams->getParamVal("typeIndicators.intervalDuration", + std::to_string(TYPE_FILTER_INTERVAL_DURATION.count())))); + params.ratioThreshold = std::stod(pParams->getParamVal("typeIndicators.ratio", + std::to_string(TYPE_FILTER_CONFIDENCE_THRESHOLD))); + std::string learnPermanentlyStr = pParams->getParamVal("typeIndicators.learnPermanently", "true"); + params.learnPermanently = !boost::iequals(learnPermanentlyStr, "false"); + + std::string remoteSyncStr = pParams->getParamVal("remoteSync", "true"); + bool syncEnabled = !boost::iequals(remoteSyncStr, "false"); + + dbgTrace(D_WAAP) << params << " remote sync: " << remoteSyncStr; + + m_confidence_calc.setRemoteSyncEnabled(syncEnabled); + m_trusted_confidence_calc.setRemoteSyncEnabled(syncEnabled); + + m_confidence_calc.reset(params); +} + +std::set TypeIndicatorFilter::getParamTypes(const std::string& canonicParam) const +{ + std::set types = m_confidence_calc.getConfidenceValues(canonicParam); + if (m_policy != nullptr) + { + std::set types_trusted = m_trusted_confidence_calc.getConfidenceValues(canonicParam, + m_policy->getNumOfSources()); + types.insert(types_trusted.begin(), types_trusted.end()); + } + return types; +} diff --git a/components/security_apps/waap/waap_clib/TypeIndicatorsFilter.h b/components/security_apps/waap/waap_clib/TypeIndicatorsFilter.h new file mode 100755 index 0000000..79ff770 --- /dev/null +++ b/components/security_apps/waap/waap_clib/TypeIndicatorsFilter.h @@ -0,0 +1,52 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include "IndicatorsFilterBase.h" +#include "WaapKeywords.h" +#include "WaapEnums.h" +#include "i_waap_asset_state.h" +#include "ConfidenceCalculator.h" +#include "WaapParameters.h" +#include + +#define TYPE_FILTER_CONFIDENCE_MIN_SOURCES 10 +#define TYPE_FILTER_CONFIDENCE_MIN_INTERVALS 5 +#define TYPE_FILTER_CONFIDENCE_THRESHOLD 0.8 +#define TYPE_FILTER_INTERVAL_DURATION std::chrono::minutes(60) + +class TypeIndicatorFilter : public IndicatorFilterBase +{ +public: + TypeIndicatorFilter(I_WaapAssetState* pWaapAssetState, + const std::string& remotePath, + const std::string& assetId, + TuningDecision* tuning = nullptr, + size_t minSources = TYPE_FILTER_CONFIDENCE_MIN_SOURCES, + size_t minIntervals = TYPE_FILTER_CONFIDENCE_MIN_INTERVALS, + std::chrono::minutes intervalDuration = TYPE_FILTER_INTERVAL_DURATION, + double ratioThreshold = TYPE_FILTER_CONFIDENCE_THRESHOLD); + ~TypeIndicatorFilter(); + + virtual void registerKeywords(const std::string& key, Waap::Keywords::KeywordsSet& keyword, + IWaf2Transaction* pTransaction); + + void registerKeywords(const std::string& key, const std::string& sample, IWaf2Transaction* pTransaction); + + void loadParams(std::shared_ptr pParams); + virtual bool shouldFilterKeyword(const std::string &keyword, const std::string &key) const; + std::set getParamTypes(const std::string& canonicParam) const; + +private: + I_WaapAssetState* m_pWaapAssetState; +}; diff --git a/components/security_apps/waap/waap_clib/UserLimitsDecision.cc b/components/security_apps/waap/waap_clib/UserLimitsDecision.cc new file mode 100755 index 0000000..68fd2dc --- /dev/null +++ b/components/security_apps/waap/waap_clib/UserLimitsDecision.cc @@ -0,0 +1,22 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "UserLimitsDecision.h" + +UserLimitsDecision::UserLimitsDecision(DecisionType type): SingleDecision(type) +{} + +std::string UserLimitsDecision::getTypeStr() const +{ + return "User Limits"; +} diff --git a/components/security_apps/waap/waap_clib/UserLimitsDecision.h b/components/security_apps/waap/waap_clib/UserLimitsDecision.h new file mode 100755 index 0000000..f16a599 --- /dev/null +++ b/components/security_apps/waap/waap_clib/UserLimitsDecision.h @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __USER_LIMITS_DECISION_H__ +#define __USER_LIMITS_DECISION_H__ + +#include "SingleDecision.h" +#include "DecisionType.h" +#include + +class UserLimitsDecision: public SingleDecision +{ +public: + explicit UserLimitsDecision(DecisionType type); + std::string getTypeStr() const override; +}; +#endif diff --git a/components/security_apps/waap/waap_clib/UserLimitsPolicy.cc b/components/security_apps/waap/waap_clib/UserLimitsPolicy.cc new file mode 100644 index 0000000..d6391af --- /dev/null +++ b/components/security_apps/waap/waap_clib/UserLimitsPolicy.cc @@ -0,0 +1,330 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "UserLimitsPolicy.h" +#include +#include + +namespace Waap { +namespace UserLimits { + +typedef unsigned long long ull; + +bool Policy::operator==(const Policy& other) const +{ + return getConfig() == other.getConfig(); +} + +bool Policy::Config::operator==(const Policy::Config& other) const +{ + return urlMaxSize == other.urlMaxSize && + httpHeaderMaxSize == other.httpHeaderMaxSize && + httpBodyMaxSize == other.httpBodyMaxSize && + maxObjectDepth == other.maxObjectDepth && + httpIllegalMethodsAllowed == other.httpIllegalMethodsAllowed; +} + +std::ostream& operator<<(std::ostream& os, const Policy& policy) +{ + auto config = policy.getConfig(); + os << "[Policy] " << "urlMaxSize: " << config.urlMaxSize << " " << + "httpHeaderMaxSize: " << config.httpHeaderMaxSize << " " << + "httpBodyMaxSize: " << config.httpBodyMaxSize << " " << + "maxObjectDepth: " << config.maxObjectDepth << " " << + std::boolalpha << "httpIllegalMethodsAllowed: " << config.httpIllegalMethodsAllowed; + return os; +} + +bool State::addUrlBytes(size_t size) +{ + setCurrStateType(StateType::URL); + if (m_urlSize > std::numeric_limits::max() - size) { + // We are about to overflow + setViolationType(ViolationType::URL_OVERFLOW); + m_urlSize = std::numeric_limits::max(); + dbgWarning(D_WAAP_ULIMITS) << "[USER LIMITS] Url size overflow. Asset id: " << getAssetId(); + return true; + } + + m_urlSize += size; + if (m_urlSize > m_policy.getUrlMaxSize()) { + setViolationType(ViolationType::URL_LIMIT); + dbgWarning(D_WAAP_ULIMITS) << "[USER LIMITS] Url size limit exceeded " << + m_urlSize << "/" << m_policy.getUrlMaxSize() << ". Asset id: " << getAssetId(); + return true; + } + dbgTrace(D_WAAP_ULIMITS) << "[USER LIMITS] Current url bytes " << m_urlSize << "/" << + m_policy.getUrlMaxSize(); + return false; +} + +bool State::addHeaderBytes(const std::string& name, const std::string& value) +{ + setCurrStateType(StateType::HEADER); + size_t chunkSize = name.size() + value.size(); + if (m_httpHeaderSize > std::numeric_limits::max() - chunkSize) { + // We are about to overflow + setViolationType(ViolationType::HEADER_OVERFLOW); + m_httpHeaderSize = std::numeric_limits::max(); + dbgWarning(D_WAAP_ULIMITS) << "[USER LIMITS] Http header size overflow. Asset id: " << getAssetId(); + return true; + } + + m_httpHeaderSize += chunkSize; + if (m_httpHeaderSize > m_policy.getHttpHeaderMaxSize()) { + setViolationType(ViolationType::HEADER_LIMIT); + dbgWarning(D_WAAP_ULIMITS) << "[USER LIMITS] Http header size limit exceeded " << + m_httpHeaderSize << "/" << m_policy.getHttpHeaderMaxSize() << ". Asset id: " << getAssetId(); + return true; + } + dbgTrace(D_WAAP_ULIMITS) << "[USER LIMITS] Current header bytes " << m_httpHeaderSize << "/" << + m_policy.getHttpHeaderMaxSize(); + return false; +} + +bool State::addBodyBytes(size_t chunkSize) +{ + setCurrStateType(StateType::BODY); + if (m_httpBodySize > std::numeric_limits::max() - chunkSize) { + // We are about to overflow + setViolationType(ViolationType::BODY_OVERFLOW); + m_httpBodySize = std::numeric_limits::max(); + dbgWarning(D_WAAP_ULIMITS) << "[USER LIMITS] Http body size overflow. Asset id: " << getAssetId(); + return true; + } + + m_httpBodySize += chunkSize; + if (m_httpBodySize > m_policy.getHttpBodyMaxSize()) { + setViolationType(ViolationType::BODY_LIMIT); + dbgWarning(D_WAAP_ULIMITS) << "[USER LIMITS] Http body size limit exceeded " << + m_httpBodySize << "/" << m_policy.getHttpBodyMaxSize() << ". Asset id: " << getAssetId(); + return true; + } + dbgTrace(D_WAAP_ULIMITS) << "[USER LIMITS] Current body bytes " << m_httpBodySize << "/" << + m_policy.getHttpBodyMaxSize(); + return false; +} + +bool State::setObjectDepth(size_t depth) +{ + setCurrStateType(StateType::DEPTH); + m_objectDepth = depth; + if (m_objectDepth > m_policy.getMaxObjectDepth()) { + setViolationType(ViolationType::OBJECT_DEPTH_LIMIT); + dbgWarning(D_WAAP_ULIMITS) << "[USER LIMITS] Http object depth limit exceeded " << + m_objectDepth << "/" << m_policy.getMaxObjectDepth() << ". Asset id: " << getAssetId(); + return true; + } + dbgTrace(D_WAAP_ULIMITS) << "[USER LIMITS] Current object depth " << m_objectDepth << "/" << + m_policy.getMaxObjectDepth(); + return false; +} + +bool State::isValidHttpMethod(const std::string& method) +{ + setCurrStateType(StateType::METHOD); + if (m_policy.isHttpIllegalMethodAllowed()) { + dbgTrace(D_WAAP_ULIMITS) << "[USER LIMITS][method: " << method << "] Http all methods allowed"; + return true; + } + + if (isLegalHttpMethod(method)) { + dbgTrace(D_WAAP_ULIMITS) << "[USER LIMITS][method: " << method << "] Http legal method"; + return true; + } + setViolationType(ViolationType::ILLEGAL_METHOD); + dbgWarning(D_WAAP_ULIMITS) << "[USER LIMITS][method: " << method << "] Http illegal method" << + ". Asset id: " << getAssetId(); + return false; +} + +bool State::isLegalHttpMethod(const std::string& method) const +{ + if (method == "GET") return true; + if (method == "POST") return true; + if (method == "DELETE") return true; + if (method == "PATCH") return true; + if (method == "PUT") return true; + if (method == "CONNECT") return true; + if (method == "OPTIONS") return true; + if (method == "HEAD") return true; + if (method == "TRACE") return true; + // Below methods are part of WebDAV http protocol extension + if (method == "MKCOL") return true; + if (method == "COPY") return true; + if (method == "MOVE") return true; + if (method == "PROPFIND") return true; + if (method == "PROPPATCH") return true; + if (method == "LOCK") return true; + if (method == "UNLOCK") return true; + if (method == "VERSION-CONTROL") return true; + if (method == "REPORT") return true; + if (method == "INDEX") return true; + if (method == "CHECKOUT") return true; + if (method == "CHECKIN") return true; + if (method == "UNCHECKOUT") return true; + if (method == "MKWORKSPACE") return true; + if (method == "UPDATE") return true; + if (method == "LABEL") return true; + if (method == "MERGE") return true; + if (method == "BASELINE-CONTROL") return true; + if (method == "MKACTIVITY") return true; + if (method == "ORDERPATCH") return true; + if (method == "ACL") return true; + if (method == "PATCH") return true; + if (method == "SEARCH") return true; + if (method == "MKREDIRECTREF") return true; + if (method == "BIND") return true; + if (method == "UNBIND") return true; + return false; +} + +bool State::isLimitReached() const +{ + return m_type != ViolationType::NO_LIMIT; +} + +bool State::isIllegalMethodViolation() const +{ + return m_type == ViolationType::ILLEGAL_METHOD; +} + +void State::setViolationType(ViolationType type) +{ + m_type = type; + setViolatedTypeStr(); + setViolatedPolicyStr(); +} + +void State::setViolatedTypeStr() +{ + std::stringstream ss; + switch (m_type) + { + case ViolationType::ILLEGAL_METHOD: { + ss << "method violation"; + break; + } + case ViolationType::URL_LIMIT: { + ss << "url size exceeded"; + break; + } + case ViolationType::URL_OVERFLOW: { + ss << "url size overflow"; + break; + } + case ViolationType::HEADER_LIMIT: { + ss << "header size exceeded"; + break; + } + case ViolationType::HEADER_OVERFLOW: { + ss << "header size overflow"; + break; + } + case ViolationType::BODY_LIMIT: { + ss << "body size exceeded"; + break; + } + case ViolationType::BODY_OVERFLOW: { + ss << "body size overflow"; + break; + } + case ViolationType::OBJECT_DEPTH_LIMIT: { + ss << "object depth exceeded"; + break; + } + default: + ss << "no violation"; + } + m_strData.type = ss.str(); +} + +void State::setViolatedPolicyStr() +{ + std::stringstream ss; + switch (m_type) + { + case ViolationType::ILLEGAL_METHOD: { + if (m_policy.isHttpIllegalMethodAllowed()) { + ss << "true"; + } + else { + ss << "false"; + } + break; + } + case ViolationType::URL_LIMIT: + case ViolationType::URL_OVERFLOW: { + ss << m_policy.getUrlMaxSize(); + if (m_policy.getUrlMaxSize() == 1) { + ss << " Byte"; + } + else { + ss << " Bytes"; + }; + break; + } + case ViolationType::HEADER_LIMIT: + case ViolationType::HEADER_OVERFLOW: { + ss << m_policy.getHttpHeaderMaxSize(); + if (m_policy.getHttpHeaderMaxSize() == 1) { + ss << " Byte"; + } + else { + ss << " Bytes"; + } + break; + } + case ViolationType::BODY_LIMIT: + case ViolationType::BODY_OVERFLOW: { + ss << m_policy.getHttpBodyMaxSizeKb(); + if (m_policy.getHttpBodyMaxSizeKb() == 1) { + ss << " Kilobyte"; + } + else { + ss << " Kilobytes"; + } + break; + } + case ViolationType::OBJECT_DEPTH_LIMIT: { + ss << m_policy.getMaxObjectDepth(); + break; + } + default: + ss << "unknown"; + } + m_strData.policy = ss.str(); +} + +size_t State::getViolatingSize() const +{ + switch (m_type) + { + case ViolationType::URL_LIMIT: + case ViolationType::URL_OVERFLOW: + return m_urlSize; + case ViolationType::HEADER_LIMIT: + case ViolationType::HEADER_OVERFLOW: + return m_httpHeaderSize; + case ViolationType::BODY_LIMIT: + case ViolationType::BODY_OVERFLOW: + return static_cast(m_httpBodySize / 1024); + case ViolationType::OBJECT_DEPTH_LIMIT: + return m_objectDepth; + default: + return 0; + } +} + +} // namespace UserLimits +} // namespace Waap diff --git a/components/security_apps/waap/waap_clib/UserLimitsPolicy.h b/components/security_apps/waap/waap_clib/UserLimitsPolicy.h new file mode 100644 index 0000000..c1332b7 --- /dev/null +++ b/components/security_apps/waap/waap_clib/UserLimitsPolicy.h @@ -0,0 +1,183 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include "debug.h" +#include +#include +#include +#include + +USE_DEBUG_FLAG(D_WAAP_ULIMITS); + +namespace Waap { +namespace UserLimits { + +typedef unsigned long long ull; +#define DEFAULT_URL_MAX_SIZE 32*1024 +#define DEFAULT_HEADER_MAX_SIZE 100*1024 +#define DEFAULT_BODY_MAX_SIZE_KB 1000000 +#define DEFAULT_BODY_MAX_SIZE 1000000*1024 +#define DEFAULT_OBJECT_MAX_DEPTH 40 + +// @file Feature behaviour description: +// Phase 1: +// 1. No enforcement. No logs to mgmt. +// 2. Only logs to automation and dev Kibana. +// 3. Logs should represent the state as if the limits are enforced as described in phase 2. +// Phase 2: +// 1. DISABLE mode: no enforcement and no logs. +// 2. LEARNING mode: requests that violated a limit will be accepted, and won't be scanned any further. +// Illegal methods won't be automatically accepted, and will be further scanned. +// 3. PREVENT mode: requests that violated a limit will be dropped, and won't be scanned any further. +class Policy { + struct Config { + Config() : + urlMaxSize(DEFAULT_URL_MAX_SIZE), + httpHeaderMaxSize(DEFAULT_HEADER_MAX_SIZE), + httpBodyMaxSizeKb(DEFAULT_BODY_MAX_SIZE_KB), + httpBodyMaxSize(DEFAULT_BODY_MAX_SIZE), + maxObjectDepth(DEFAULT_OBJECT_MAX_DEPTH), + httpIllegalMethodsAllowed(false) {} + ~Config() {} + + template + void serialize(_A& ar) { + ar(cereal::make_nvp("urlMaxSize", urlMaxSize)); + ar(cereal::make_nvp("httpHeaderMaxSize", httpHeaderMaxSize)); + httpBodyMaxSizeKb = 0; + ar(cereal::make_nvp("httpRequestBodyMaxSize", httpBodyMaxSizeKb)); + // Kilobytes to bytes conversion + httpBodyMaxSize = httpBodyMaxSizeKb * 1024; + ar(cereal::make_nvp("jsonMaxObjectDepth", maxObjectDepth)); + int intToBool = 0; + ar(cereal::make_nvp("httpIllegalMethodsAllowed", intToBool)); + httpIllegalMethodsAllowed = (intToBool == 1); + } + + bool operator==(const Policy::Config& other) const; + + size_t urlMaxSize; // URL max size in bytes + size_t httpHeaderMaxSize; // Header Size in Bytes + size_t httpBodyMaxSizeKb; // Body Size in Kilobytes + ull httpBodyMaxSize; // Body Size in Bytes + size_t maxObjectDepth; // Can range from 0 to 1024 + // List of legal methods can be viewed in isLegalHttpMethod function + bool httpIllegalMethodsAllowed; + }; +public: + template + explicit Policy(_A& ar) + { + ar(cereal::make_nvp("practiceAdvancedConfig", m_config)); + } + Policy() : m_config() {} + ~Policy() {} + + bool operator==(const Policy& other) const; + size_t getUrlMaxSize() const { return m_config.urlMaxSize; } + size_t getMaxObjectDepth() const { return m_config.maxObjectDepth; } + size_t getHttpHeaderMaxSize() const { return m_config.httpHeaderMaxSize; } + size_t getHttpBodyMaxSizeKb() const { return m_config.httpBodyMaxSizeKb; } + ull getHttpBodyMaxSize() const { return m_config.httpBodyMaxSize; } + bool isHttpIllegalMethodAllowed() const { return m_config.httpIllegalMethodsAllowed; } + const Config& getConfig() const { return m_config; } + +private: + Config m_config; + + friend std::ostream& operator<<(std::ostream& os, const Policy& policy); +}; + +struct ViolatedStrData +{ + std::string type; + std::string policy; + std::string assetId; +}; + +class State { +public: + enum class StateType + { + NO_STATE, + URL, + METHOD, + HEADER, + BODY, + DEPTH + }; + + enum class ViolationType + { + NO_LIMIT, + ILLEGAL_METHOD, + URL_LIMIT, + URL_OVERFLOW, + HEADER_LIMIT, + HEADER_OVERFLOW, + BODY_LIMIT, + BODY_OVERFLOW, + OBJECT_DEPTH_LIMIT + }; +public: + explicit State(const Policy& policy) : + m_policy(policy), + m_urlSize(0), + m_httpHeaderSize(0), + m_httpBodySize(0), + m_objectDepth(0), + m_currState(StateType::NO_STATE), + m_type(ViolationType::NO_LIMIT), + m_strData() + { + m_strData.type = "no violation"; + } + ~State() {} + + void setAssetId(const std::string& assetId) { m_strData.assetId = assetId; } + // @return true if limit is reached or overflows + bool addUrlBytes(size_t size); + bool addHeaderBytes(const std::string& name, const std::string& value); + bool addBodyBytes(size_t chunkSize); + // @return true if limit is reached + bool setObjectDepth(size_t depth); + bool isValidHttpMethod(const std::string& method); + bool isLimitReached() const; + bool isIllegalMethodViolation() const; + const std::string getViolatedTypeStr() const { return m_strData.type; } + const ViolatedStrData& getViolatedStrData() const { return m_strData; } + size_t getViolatingSize() const; + +private: + bool isLegalHttpMethod(const std::string& method) const; + void setCurrStateType(StateType type) { m_currState = type; } + StateType getCurrStateType() { return m_currState; } + void setViolationType(ViolationType type); + void setViolatedTypeStr(); + void setViolatedPolicyStr(); + const std::string& getAssetId() const { return m_strData.assetId; } + +private: + const Policy& m_policy; + size_t m_urlSize; + size_t m_httpHeaderSize; + ull m_httpBodySize; + size_t m_objectDepth; + StateType m_currState; // State that is currently being enforced + ViolationType m_type; // Type of violation reached + ViolatedStrData m_strData; // Holds the string info of the violated data +}; + +} +} diff --git a/components/security_apps/waap/waap_clib/WaapAssetState.cc b/components/security_apps/waap/waap_clib/WaapAssetState.cc new file mode 100755 index 0000000..73dcb9c --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapAssetState.cc @@ -0,0 +1,1965 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// #define WAF2_LOGGING_ENABLE (does performance impact) +#include "WaapAssetState.h" +#include "Waf2Regex.h" +#include "debug.h" +#include "Waf2Util.h" +#include "maybe_res.h" +#include "picojson.h" +#include "agent_core_utilities.h" +#include +#include +#include + +#define MAX_CACHE_VALUE_SIZE 1024 + +USE_DEBUG_FLAG(D_WAAP_ASSET_STATE); +USE_DEBUG_FLAG(D_WAAP_SAMPLE_PREPROCESS); +USE_DEBUG_FLAG(D_WAAP_SAMPLE_SCAN); +USE_DEBUG_FLAG(D_WAAP_EVASIONS); + +typedef picojson::value::object JsObj; +typedef picojson::value JsVal; +typedef picojson::value::array JsArr; +typedef std::map> filtered_parameters_t; + +#ifdef WAF2_LOGGING_ENABLE +static void +print_filtered(std::string title, const std::set& ignored_set, const std::vector& v) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "--------------------------"; +#if 0 // TODO:: may be useful for debug, but in general no need to print this on every scanned value... + dbgTrace(D_WAAP_SAMPLE_SCAN) << "Ignored " << title << " set:"; + for (std::set::const_iterator it = ignored_set.begin(); it != ignored_set.end(); ++it) { + const std::string& word = *it; + dbgTrace(D_WAAP_SAMPLE_SCAN) << "*'" << word << "'"; + } +#endif + dbgTrace(D_WAAP_SAMPLE_SCAN) << title << " collected:"; + for (std::vector::const_iterator it = v.begin(); it != v.end(); ++it) { + const std::string& word = *it; + + if (ignored_set.find(word) == ignored_set.end()) { + // not in ignored_set + dbgTrace(D_WAAP_SAMPLE_SCAN) << "+'" << word << "'"; + } + else { + // in ignored set + dbgTrace(D_WAAP_SAMPLE_SCAN) << "-'" << word << "'"; + } + } + dbgTrace(D_WAAP_SAMPLE_SCAN) << "--------------------------"; +} + +static void print_found_patterns(const Waap::Util::map_of_stringlists_t& m) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "-- found_patterns: ---------"; + for (auto g = m.begin(); g != m.end(); ++g) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "'" << g->first << "'"; + for (auto p = g->second.begin(); p != g->second.end(); ++p) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << " `-> '" << (*p) << "'"; + } + } + dbgTrace(D_WAAP_SAMPLE_SCAN) << "--------------------------"; +} +#endif + +static bool err_hex = false; +static const SingleRegex evasion_hex_regex( + "(0x[0-9a-f][0-9a-f])[\\w.%?*\\/\\\\]|[\\w.%?*\\/\\\\](0x[0-9a-f][0-9a-f])", + err_hex, + "evasion_hex_regex"); +static const boost::regex bad_hex_regex = boost::regex("%[cC]1%[19][cC]"); +static const SingleRegex evasion_bad_hex_regex( + "(%[cC]1%[19][cC])[\\w.%?*\\/\\\\]|[\\w.%?*\\/\\\\](%[cC]1%[19][cC])", + err_hex, + "evasion_bad_hex_regex"); + +WaapAssetState::WaapAssetState(const std::shared_ptr& pWaapAssetState, + const std::string& sigScoresFname, + const std::string& id) : + WaapAssetState(pWaapAssetState->m_Signatures, + sigScoresFname, + pWaapAssetState->m_cleanValuesCache.capacity(), + pWaapAssetState->m_suspiciousValuesCache.capacity(), + pWaapAssetState->m_sampleTypeCache.capacity(), + id) +{ + scoreBuilder.mergeScores(pWaapAssetState->scoreBuilder); + updateScores(); + m_typeValidator = pWaapAssetState->m_typeValidator; + + registerConfigLoadCb( + [this]() + { + clearRateLimitingState(); + clearSecurityHeadersState(); + clearErrorLimitingState(); + } + ); +} + +WaapAssetState::WaapAssetState(std::shared_ptr signatures, + const std::string& sigScoresFname, + size_t cleanValuesCacheCapacity, + size_t suspiciousValuesCacheCapacity, + size_t sampleTypeCacheCapacity, + const std::string& assetId) : + m_Signatures(signatures), + m_SignaturesScoresFilePath(sigScoresFname), + m_assetId(assetId), + scoreBuilder(this), + m_rateLimitingState(nullptr), + m_errorLimitingState(nullptr), + m_securityHeadersState(nullptr), + m_filtersMngr(nullptr), + m_typeValidator(getSignaturesFilterDir() + "/8.data"), + m_cleanValuesCache(cleanValuesCacheCapacity), + m_suspiciousValuesCache(suspiciousValuesCacheCapacity), + m_sampleTypeCache(sampleTypeCacheCapacity) + { + if (assetId != "" && Singleton::exists()) + { + I_AgentDetails* agentDetails = Singleton::Consume::by(); + std::string path = agentDetails->getTenantId() + "/" + assetId; + m_filtersMngr = std::make_shared(path, assetId, this); + } + else + { + m_filtersMngr = std::make_shared("", "", this); + } + // Load keyword scores - copy from ScoreBuilder + updateScores(); + } + + WaapAssetState::~WaapAssetState() { + // TODO:: leaving this uncommented may introduce (not critical) memory leak. + // Should return this code after testing it well. +#if 0 + // clean up the headers_re map to avoid memory leak + for (auto it = m_Signatures->headers_re.begin(); it != m_Signatures->headers_re.end(); ++it) { + delete it->second; // delete allocated Regex instances + } +#endif + } + + std::shared_ptr WaapAssetState::getSignatures() const + { + return m_Signatures; + } + + + void WaapAssetState::reset() + { + m_filtersMngr->reset(); + } + + void filterUnicode(std::string & text) { + std::string::iterator it = text.begin(); + std::string::iterator result = it; + uint32_t acc = 0; + int bytes_left = 0; + + for (; it != text.end(); ++it) { + unsigned char ch = (unsigned char)(*it); + + // If character high bits are 10xxxxxx, then it might be UTF-8 character used to evade. + // For example 0xc0, 0xaf may mean '/' in broken utf-8 decoders + // In our implementation we do remove leading byte in UTF8 encoding (such as 0xc0), + // but strip down the following bytes (with high bits 01). + if (ch <= 127) { + *result++ = ch; + bytes_left = 0; // any character <= 127 stops collecting UTF8 code + } + else { + if (bytes_left == 0) { + // collect utf8 code + if ((ch & 0xE0) == 0xC0) { // 110X XXXX two bytes follow + + if ((ch & 0x1E) != 0) { + acc = ch & 31; + } + bytes_left = 1; + } + else if ((ch & 0xF0) == 0xE0) { // 1110 XXXX three bytes follow + acc = ch & 15; + bytes_left = 2; + } + else if ((ch & 0xF8) == 0xF0) { // 1111 0XXX four bytes follow + acc = ch & 7; + bytes_left = 3; + } + else if ((ch & 0xFC) == 0xF8) { // 1111 10XX five bytes follow (by standard -an error) + acc = ch & 3; + bytes_left = 4; + } + else if ((ch & 0xFE) == 0xFC) { // 1111 110X six bytes follow (by standard -an error) + acc = ch & 1; + bytes_left = 5; + } + else { + // error + bytes_left = 0; + } + } + else if (bytes_left > 0) { + // "good" encoder would check that the following bytes contain "10" as their high bits, + // but buggy encoders don't, so are we! + acc = (acc << 6) | (ch & 0x3F); + bytes_left--; + + if (bytes_left == 0) { + // finished collecting the utf8 code + if (acc <= 127) { + *result++ = acc; + } + else if (isSpecialUnicode(acc)) { + *result++ = convertSpecialUnicode(acc); + } + acc = 0; + } + } + } + } + + text.erase(result, text.end()); + } + +#if 0 + //std::replace_if(text.begin(), text.end(), [](char c) { return !(c>=0); }, ' '); + inline void replaceUnicode(std::string & text, const char repl) { + std::string::iterator it = text.begin(); + + for (; it != text.end(); ++it) { + if (*it < 0) { + *it = repl; + } + } + } +#endif + + // Python equivalent: text = re.sub(r'[^\x00-\x7F]+',' ', text) + void replaceUnicodeSequence(std::string & text, const char repl) { + std::string::iterator it = text.begin(); + std::string::iterator result = it; + uint32_t acc = 0; + int bytes_left = 0; + + for (; it != text.end(); ++it) { + unsigned char ch = (unsigned char)(*it); + + // If character high bits are 10xxxxxx, then it might be UTF-8 character used to evade. + // For example 0xc0, 0xaf may mean '/' in broken utf-8 decoders + // In our implementation we do remove leading byte in UTF8 encoding (such as 0xc0), + // but strip down the following bytes (with high bits 01). + if (ch <= 127) { + *result++ = ch; + bytes_left = 0; // any character <= 127 stops collecting UTF8 code + } + else { + if (bytes_left == 0) { + // collect utf8 code + if ((ch & 0xE0) == 0xC0) { // 110X XXXX two bytes follow + if ((ch & 0x1E) != 0) { + acc = ch & 31; + } + bytes_left = 1; + } + else if ((ch & 0xF0) == 0xE0) { // 1110 XXXX three bytes follow + acc = ch & 15; + bytes_left = 2; + } + else if ((ch & 0xF8) == 0xF0) { // 1111 0XXX four bytes follow + acc = ch & 7; + bytes_left = 3; + } + else if ((ch & 0xFC) == 0xF8) { // 1111 10XX five bytes follow (by standard -an error) + acc = ch & 3; + bytes_left = 4; + } + else if ((ch & 0xFE) == 0xFC) { // 1111 110X six bytes follow (by standard -an error) + acc = ch & 1; + bytes_left = 5; + } + else { + // error + bytes_left = 0; + } + } + else if (bytes_left > 0) { + // "good" encoder would check that the following bytes contain "10" as their high bits, + // but buggy encoders don't, so are we! + acc = (acc << 6) | (ch & 0x3F); + bytes_left--; + + if (bytes_left == 0) { + // finished collecting the utf8 code + if (acc <= 127) { + *result++ = acc; + } + else if (isSpecialUnicode(acc)) { + *result++ = convertSpecialUnicode(acc); + } + else { + *result++ = repl; + } + acc = 0; + } + } + } + } + + text.erase(result, text.end()); + } + + void + fixBreakingSpace(std::string &line) + { + for (char &c : line) { + if (c == (char)0xA0) { // "non-breaking space" + c = ' '; // convert to normal space + } + } + } + + std::string unescape(const std::string & s) { + std::string text = s; + dbgTrace(D_WAAP_SAMPLE_PREPROCESS) << "unescape: (0) '" << text << "'"; + + fixBreakingSpace(text); + // 1. remove all unicode characters from string. Basically, + // remove all characters whose ASCII code is >=128. + // Python equivalent: text.encode('ascii',errors='ignore') + filterUnicode(text); + dbgTrace(D_WAAP_SAMPLE_PREPROCESS) << "unescape: (1) '" << text << "'"; + + text = filterUTF7(text); + dbgTrace(D_WAAP_SAMPLE_PREPROCESS) << "unescape: (1) (after filterUTF7) '" << text << "'"; + + // 2. Replace %xx sequences by their single-character equivalents. + // Also replaces '+' symbol by space character. + // Python equivalent: text = urllib.unquote_plus(text) + text.erase(unquote_plus(text.begin(), text.end()), text.end()); + dbgTrace(D_WAAP_SAMPLE_PREPROCESS) << "unescape: (2) '" << text << "'"; + + fixBreakingSpace(text); + + // 3. remove all unicode characters from string. Basically, + // remove all characters whose ASCII code is >=128. + // Python equivalent: text.encode('ascii',errors='ignore') + filterUnicode(text); + dbgTrace(D_WAAP_SAMPLE_PREPROCESS) << "unescape: (3) '" << text << "'"; + + // 4. oh shi?... should I handle unicode html entities (python's htmlentitydefs module)??? + // Python equivalent: text = HTMLParser.HTMLParser().unescape(text) + text.erase(escape_html(text.begin(), text.end()), text.end()); + dbgTrace(D_WAAP_SAMPLE_PREPROCESS) << "unescape: (4) '" << text << "'"; + + // 5. Apply backslash escaping (like in C) + // Python equivalent: text = text.decode('string_escape') + text.erase(escape_backslashes(text.begin(), text.end()), text.end()); + dbgTrace(D_WAAP_SAMPLE_PREPROCESS) << "unescape: (5) '" << text << "'"; + + // 6. remove all unicode characters from string. Basically, + // remove all characters whose ASCII code is >=128. + // Python equivalent: text.encode('ascii',errors='ignore') + filterUnicode(text); + dbgTrace(D_WAAP_SAMPLE_PREPROCESS) << "unescape: (6) '" << text << "'"; + + // 7. Replace %xx sequences by their single-character equivalents. + // Also replaces '+' symbol by space character. + // Python equivalent: text = urllib.unquote_plus(text) + text.erase(unquote_plus(text.begin(), text.end()), text.end()); + dbgTrace(D_WAAP_SAMPLE_PREPROCESS) << "unescape: (7) '" << text << "'"; + + unescapeUnicode(text); + dbgTrace(D_WAAP_SAMPLE_PREPROCESS) << "after unescapeUnicode '" << text << "'"; + + // 8. remove all unicode characters from string. Basically, + // remove all characters whose ASCII code is >=128. + // Python equivalent: text.encode('ascii',errors='ignore') + filterUnicode(text); + dbgTrace(D_WAAP_SAMPLE_PREPROCESS) << "unescape: (8) '" << text << "'"; + + // 9. ??? + // + //try: + // text = text.decode('utf-8') + //except: + // pass + + // 10. Replace each sequence of unicode characters with single space + // Python equivalent: text = re.sub(r'[^\x00-\x7F]+',' ', text) + // TODO:: actually, in python Pavel do this: + // text = re.sub(r'[^\x00-\x7F]+',' ', text).encode("ascii","ignore") + replaceUnicodeSequence(text, ' '); + +#if 0 // Removed Aug 25 2018. Reason for removal - breaks input containing ASCII zeros. + // 11. remove all unicode characters from string. + // Basically, remove all characters whose ASCII code is >=128. + // Python equivalent: text.encode('ascii',errors='ignore') + filterUnicode(text); +#endif + + dbgTrace(D_WAAP_SAMPLE_PREPROCESS) << "unescape: (11) '" << text << "'"; + + // 12. finally, apply tolower() to all characters of a string + // std::for_each(text.begin(), text.end(), [](char &c) { c = tolower(c); }); + for (std::string::iterator pC = text.begin(); pC != text.end(); ++pC) { + *pC = tolower(*pC); + } + + dbgTrace(D_WAAP_SAMPLE_PREPROCESS) << "unescape: (12) '" << text << "'"; + return text; + } + + inline std::string repr_uniq(const std::string & value) { + std::string result; + char hist[256]; + memset(&hist, 0, sizeof(hist)); + + for (std::string::const_iterator pC = value.begin(); pC != value.end(); ++pC) { + unsigned char ch = (unsigned char)(*pC); + + // Only take ASCII characters that are not alphanumeric, and each character only once + if (ch <= 127 && !isalnum(ch) && hist[ch] == 0) { + // Convert low ASCII characters to their C/C++ printable equivalent + // (used for easier viewing. Also, binary data causes issues with ElasticSearch) + switch (ch) { + case 0x07: result += "\\a"; break; + case 0x08: result += "\\b"; break; + case 0x09: result += "\\t"; break; + case 0x0A: result += "\\n"; break; + case 0x0B: result += "\\v"; break; + case 0x0C: result += "\\f"; break; + case 0x0D: result += "\\r"; break; + case 0x5C: result += "\\\\"; break; + case 0x27: result += "\\\'"; break; + case 0x22: result += "\\\""; break; + case 0x3F: result += "\\\?"; break; + default: { + if (ch >= 32) { + result += (char)ch; + } + else { + char buf[16]; + sprintf(buf, "\\" "x%02X", ch); + result += buf; + } + } + } + + hist[ch] = 1; + } + } + + return result; + } + + static bool isShortWord(const std::string &word) + { + return word.size() <= 2; + } + + static bool isShortHtmlTag(const std::string &word) + { + return !word.empty() && word.size() <= 3 && word[0] == '<'; + } + + void + WaapAssetState::checkRegex( + const SampleValue &sample, + const Regex & pattern, + std::vector& keyword_matches, + Waap::Util::map_of_stringlists_t & found_patterns, + bool longTextFound, + bool binaryDataFound) const + { + dbgFlow(D_WAAP_SAMPLE_SCAN) << "checkRegex: line='" << sample.getSampleString() << "' patt='" << + pattern.getName() << "' longTextFound=" << longTextFound << " binaryDataFound=" << binaryDataFound; + + std::vector matches; + sample.findMatches(pattern, matches); + + for (std::vector::const_iterator pMatch = matches.begin(); pMatch != matches.end(); ++pMatch) { + const RegexMatch& match = *pMatch; + + // Get whole match (group[0], which is always present in any match) + std::string word = match.groups.front().value; + + dbgTrace(D_WAAP_SAMPLE_SCAN) << "checkRegex: match='" << word << "':"; + + // Short words matched by regexes wont be detected in some cases like + // if enough binary data is present in the value. + if (binaryDataFound && word.size() <= 2) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "Will not add a short keyword '" << word << + "' because binaryData was found"; + continue; + } + + for (std::vector::const_iterator pGroup = match.groups.begin() + 1; + pGroup != match.groups.end(); + ++pGroup) { + std::string group = pGroup->name; + + if (group == "") { + continue; // skip unnamed group + } + + const std::string& value = pGroup->value; + dbgTrace(D_WAAP_SAMPLE_SCAN) << "checkRegex: group name='" << group << + "' value='" << value << "', word='" << word << "':"; + + // python: + // if 'fast_reg' in group: + // if 'evasion' in group: + // word = repr(str(''.join(set(value)))) + // else: + // word =group + if (group.find("fast_reg") != std::string::npos) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "checkRegex: found '*fast_reg*' in group name"; + if (group.find("evasion") != std::string::npos) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << + "checkRegex: found both 'fast_reg' and 'evasion' in group name."; + + word = "encoded_" + repr_uniq(value); + + // Normally, the word added to the keyword_matches list contain the character sequence. + // However, sometimes (for example in case the sequence contained only unicode characters), + // after running repr_uniq() the word will remain empty string. In this case leave + // something meaningful/readable there. + if (word == "encoded_") { + dbgTrace(D_WAAP_SAMPLE_SCAN) << + "checkRegex: empty word after repr_uniq: resetting word to 'character_encoding'" + " and group to 'evasion'."; + word = "character_encoding"; + } + else if (Waap::Util::str_isalnum(word)) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << + "checkRegex: isalnum word after repr_uniq: resetting group to 'evasion'."; + // If the found match is alphanumeric (we've seen strings like "640x480" match) + // we still should assume evasion but it doesn't need to include "fast_reg", + // which would cause unconditional report to stage2 and hit performance... + // This is why we remove the word "fast_reg" from the group name. + group = "evasion"; + } + + if (longTextFound) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << + "checkRegex: longTextFound so resetting group name to 'longtext'"; + group = "longtext"; + } + } + else { + word = group; + } + } + + // In sequences detected as "longTextFound" or "longBinaryFound", do not add words in the + // "keyword_matches" list that: + // - starts with "encoded_" + // - or startswith("\") + // - or equal to "character_encoding" + if ((longTextFound || binaryDataFound) && + (word == "character_encoding" || word.substr(0, 1) == "\\" || word.substr(0, 8) == "encoded_")) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "Not adding keyword '" << word << "' because longtext was found"; + } + else if (binaryDataFound && (isShortWord(word) || isShortHtmlTag(word) || + NGEN::Regex::regexMatch(__FILE__, __LINE__, group, m_Signatures->binary_data_kw_filter))) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "Not adding group='" << group << "', word='" << word << + "' - due to binary data"; + continue; + } + else if ((std::find( + keyword_matches.begin(), + keyword_matches.end(), + word) == keyword_matches.end())) { + // python: if (word not in current_matches): current_matches.append(word) + keyword_matches.push_back(word); + } + + // python: + // if group not in found_patterns: + // found_patterns[group]=[] + if (found_patterns.find(group) == found_patterns.end()) { + found_patterns[group] = std::vector(); + } + + // python: + // if value not in found_patterns[group]: + // found_patterns[group].append(value) + if (std::find( + found_patterns[group].begin(), + found_patterns[group].end(), + value + ) == found_patterns[group].end()) { + found_patterns[group].push_back(value); + } + } + } + } + + // TODO:: implement onload mechanism. + static bool isOnLoad = 0; + +static void calcRepeatAndWordsCount(const std::string &line, unsigned int &repeat, unsigned int &wordsCount) +{ + repeat = 0; + wordsCount = 0; + int prev = -1; + int prevPrev = -1; + + for (std::string::const_iterator pC = line.begin(); pC != line.end(); ++pC) { + if (*pC == prev || *pC == prevPrev) { + repeat++; + } + + if (Waap::Util::isAlphaAsciiFast(*pC) && !Waap::Util::isAlphaAsciiFast(prev)) { + wordsCount++; + } + + prevPrev = prev; + prev = *pC; + } +} + +static void calcRepetitionAndProbing(Waf2ScanResult &res, const std::set *ignored_keywords, + const std::string &line, bool &detectedRepetition, bool &detectedProbing, unsigned int &wordsCount) +{ + unsigned int repeat; + calcRepeatAndWordsCount(line, repeat, wordsCount); + + if (!detectedRepetition && repeat>100) { // detect potential buffer overflow attacks + dbgTrace(D_WAAP_SAMPLE_SCAN) << "repetition detected: repeat=" << repeat; + detectedRepetition = true; + res.keyword_matches.push_back("repetition"); + } + + // python: + // keywords_num = sum(1 for x in keyword_matches if x not in ignored_keywords) + size_t keywords_num = countNotInSet(res.keyword_matches, *ignored_keywords); + + dbgTrace(D_WAAP_SAMPLE_SCAN) << "wordsCount: " << wordsCount << ", repeat=" << repeat + << ", keyword_matches(num=" << keywords_num << ", size=" << res.keyword_matches.size() << ")"; + + if (!detectedProbing //res.keyword_matches.size() + && keywords_num + 2 > wordsCount + // res.keyword_matches.size() + && keywords_num != 0) + { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "probing detected: keywords_num=" << keywords_num << + ", wordsCount=" << wordsCount; + detectedProbing = true; + res.keyword_matches.push_back("probing"); + } +} + +void +WaapAssetState::filterKeywordsDueToLongText(Waf2ScanResult &res) const +{ + // Test for long value without spaces (these can often cause false alarms) + if (m_Signatures->nospaces_long_value_re.hasMatch(res.unescaped_line)) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "nospaces_long_value matched. may remove some keywords below..."; + // remove some keywords that are often present in such long lines + std::vector &v = res.keyword_matches; + for (std::vector::iterator it = v.begin(); it != v.end();) { + std::string &word = *it; + if (m_Signatures->ignored_for_nospace_long_value.find(word) != + m_Signatures->ignored_for_nospace_long_value.end()) { + dbgTrace(D_WAAP_SAMPLE_SCAN) + << "Removing keyword '" + << word + << "' because nospaces_long_value was found"; + it = v.erase(it); + } + else { + ++it; + } + } + } + +#ifdef WAF2_LOGGING_ENABLE + // Dump interesting statistics and scores + print_filtered("keywords", *ignored_keywords, res.keyword_matches); + print_found_patterns(res.found_patterns); + dbgTrace(D_WAAP_SAMPLE_SCAN) << "keyword_matches.size()=" << res.keyword_matches.size(); +#endif +} + +bool +checkBinaryData(const std::string &line, bool binaryDataFound) +{ + // Test whether count of non-printable characters in the parameter value is too high. + // Note that high-ASCII characters (>=128) are assumed "printable". + // All non-ASCII UTF-8 characters fall into this cathegory. + if (!binaryDataFound && line.size() > 25) { + size_t nonPrintableCharsCount = 0; + + for (size_t i=0; i splitType) const +{ + dbgTrace(D_WAAP_SAMPLE_SCAN) + << "WaapAssetState::apply('" + << line + << "', scanStage=" + << scanStage + << ", splitType='" + << (splitType.ok() ? *splitType: "") + << "'"; + + // Handle response scan stages + if (scanStage == "resp_body") { + res.clear(); + SampleValue sample(line, nullptr); + checkRegex(sample, + m_Signatures->resp_body_words_regex_list, + res.keyword_matches, + res.found_patterns, + false, + false); + checkRegex(sample, + m_Signatures->resp_body_pattern_regex_list, + res.keyword_matches, + res.found_patterns, + false, + false); + dbgTrace(D_WAAP_SAMPLE_SCAN) << "WaapAssetState::apply(): response body " << + (res.keyword_matches.empty() ? "is not" : "is") << " suspicious"; + return !res.keyword_matches.empty(); + } + + if (scanStage == "resp_header") { + res.clear(); + SampleValue sample(line, nullptr); + checkRegex(sample, + m_Signatures->resp_body_words_regex_list, + res.keyword_matches, + res.found_patterns, + false, + false); + checkRegex(sample, + m_Signatures->resp_body_pattern_regex_list, + res.keyword_matches, + res.found_patterns, + false, + false); + dbgTrace(D_WAAP_SAMPLE_SCAN) << "WaapAssetState::apply(): response header " << + (res.keyword_matches.empty() ? "is not" : "is") << " suspicious"; + return !res.keyword_matches.empty(); + } + + // Only cache values less or equal than MAX_CACHE_VALUE_SIZE + bool shouldCache = (line.size() <= MAX_CACHE_VALUE_SIZE); + + if (shouldCache) { + // Handle cached clean values + CacheKey cache_key(line, scanStage, isBinaryData, splitType.ok() ? *splitType : ""); + if (m_cleanValuesCache.exist(cache_key)) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "WaapAssetState::apply('" << line << "'): not suspicious (cache)"; + res.clear(); + return false; + } + + // Handle cached suspicious values (if found - fills out the "res" structure) + if (m_suspiciousValuesCache.get(cache_key, res)) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "WaapAssetState::apply('" << line << "'): suspicious (cache)"; + +#ifdef WAF2_LOGGING_ENABLE + // Dump cached result + print_filtered("keywords", std::set(), res.keyword_matches); + print_filtered("patterns", std::set(), res.regex_matches); + print_found_patterns(res.found_patterns); +#endif + return true; + } + } + + dbgTrace(D_WAAP_SAMPLE_SCAN) << "WaapAssetState::apply('" << line << "'): passed the cache check."; + + const std::set* ignored_keywords = &m_Signatures->global_ignored_keywords; + const std::set* ignored_patterns = &m_Signatures->global_ignored_patterns; + bool isUrlScanStage = false; + bool isHeaderScanStage = false; + + if ((scanStage.size() == 3 && scanStage == "url") || (scanStage.size() == 7 && scanStage == "referer")) { + if (m_Signatures->url_ignored_re.hasMatch(line)) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "WaapAssetState::apply('" << line << "'): ignored for URL."; + + if (shouldCache) { + m_cleanValuesCache.insert(CacheKey(line, scanStage, isBinaryData, splitType.ok() ? *splitType : "")); + } + + res.clear(); + return false; + } + + ignored_keywords = &m_Signatures->url_ignored_keywords; + ignored_patterns = &m_Signatures->url_ignored_patterns; + isUrlScanStage = true; + } + else if ((scanStage.size() == 6 && scanStage == "header") || + (scanStage.size() == 6 && scanStage == "cookie")) { + if (m_Signatures->header_ignored_re.hasMatch(line)) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "WaapAssetState::apply('" << line << "'): ignored for header."; + + if (shouldCache) { + m_cleanValuesCache.insert(CacheKey(line, scanStage, isBinaryData, splitType.ok() ? *splitType : "")); + } + + res.clear(); + return false; + } + + ignored_keywords = &m_Signatures->header_ignored_keywords; + ignored_patterns = &m_Signatures->header_ignored_patterns; + isHeaderScanStage = true; + } + +#if 0 + // Removed by Pavel's request. Leaving here in case he'll want to add this back... + //// Pavel told me he wants to use "global" settings for cookie values, rather than cookie-specific ones here. + //else if (scanStage.size() == 6 && (scanStage == "cookie")) { + // if (cookie_ignored_re.hasMatch(line)) { + // dbgTrace(D_WAAP_SAMPLE_SCAN) << "WaapAssetState::apply('" << line << "'): ignored for cookie."; + // if (shouldCache) { + // m_cleanValuesCache.insert(CacheKey(line, scanStage)); + // } + // res.clear(); + // return false; + // } + + // ignored_keywords = &cookie_ignored_keywords; + // ignored_patterns = &cookie_ignored_patterns; + //} +#endif + +// Only perform these checks under load + if (isOnLoad) { + // Skip values that are too short + if (line.length() < 3) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "WaapAssetState::apply('" << line << + "'): skipping: did not pass the length check."; + + if (shouldCache) { + m_cleanValuesCache.insert(CacheKey(line, scanStage, isBinaryData, splitType.ok() ? *splitType : "")); + } + + res.clear(); + return false; + } + + // Skip values where all characters are alphanumeric + bool allAlNum = true; + + for (std::string::const_iterator pC = line.begin(); pC != line.end(); ++pC) { + if (!isalnum(*pC)) { + allAlNum = false; + break; + } + } + + if (allAlNum) { + if (shouldCache) { + m_cleanValuesCache.insert(CacheKey(line, scanStage, isBinaryData, splitType.ok() ? *splitType : "")); + } + + res.clear(); + return false; + } + + dbgTrace(D_WAAP_SAMPLE_SCAN) << "WaapAssetState::apply('" << line << "'): passed the stateless checks."; + + // Skip values that are longer than 10 characters, and match allowed_text_re regex + if (line.length() > 10) { + if (m_Signatures->allowed_text_re.hasMatch(line) > 0) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "WaapAssetState::apply('" << line << + "'): matched on allowed_text - ignoring."; + + if (shouldCache) { + m_cleanValuesCache.insert( + CacheKey(line, scanStage, isBinaryData, splitType.ok() ? *splitType : "") + ); + } + + res.clear(); + return false; + } + } + } + + std::string unquote_line = line; + unquote_line.erase(unquote_plus(unquote_line.begin(), unquote_line.end()), unquote_line.end()); + + // If binary data type is detected outside the scanner - enable filtering specific matches/keywords + bool binaryDataFound = + checkBinaryData(unquote_line, isBinaryData) || + checkBinaryData(line, isBinaryData); + + // Complex unescape and then apply lowercase + res.unescaped_line = unescape(line); + + dbgTrace(D_WAAP_SAMPLE_SCAN) << "unescapedLine: '" << res.unescaped_line << "'"; + + // Detect long text spans, and also any-length spans that end with file extensions such as ".jpg" + bool longTextFound = m_Signatures->longtext_re.hasMatch(res.unescaped_line); + + if (longTextFound) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "longtext found"; + } + + dbgTrace(D_WAAP_SAMPLE_SCAN) << "doing first set of checkRegex calls..."; + + // Scan unescaped_line with aho-corasick once, and reuse it in multiple calls to checkRegex below + // This is done to improve performance of regex matching. + SampleValue unescapedLineSample(res.unescaped_line, m_Signatures->m_regexPreconditions); + + checkRegex( + unescapedLineSample, + m_Signatures->specific_acuracy_keywords_regex, + res.keyword_matches, + res.found_patterns, + longTextFound, + binaryDataFound + ); + checkRegex(unescapedLineSample, m_Signatures->words_regex, res.keyword_matches, res.found_patterns, longTextFound, + binaryDataFound); + + filterKeywordsDueToLongText(res); + + bool detectedRepetition = false; + bool detectedProbing = false; + unsigned int wordsCount = 0; + + // Calculate repetition and/or probing indicators + if (!binaryDataFound) { + calcRepetitionAndProbing(res, ignored_keywords, res.unescaped_line, detectedRepetition, detectedProbing, + wordsCount); + } + + // List of keywords to remove + std::vector keywordsToRemove; + + // Handle semicolon and pipe-split values. + // Specifically exclude split cookie values to avoid high-probability high-impact false positives. + // note: All-digits values triggers fp when prepended with separator, so they are excluded + if (scanStage != "cookie" && splitType.ok() && !Waap::Util::isAllDigits(res.unescaped_line)) { + dbgTrace(D_WAAP_EVASIONS) << "split value detected type='" << *splitType << "' value='" << line << "'"; + + // Split value detected eligible for special handling. Scan it after prepending the appropriate prefix + std::string unescaped; + + std::set keywords_to_filter { + "probing", + "os_cmd_sep_medium_acuracy" + }; + + if (*splitType == "sem") { + keywords_to_filter.insert(";"); + unescaped = ";" + res.unescaped_line; + } else if (*splitType == "pipe") { + keywords_to_filter.insert("|"); + unescaped = "|" + res.unescaped_line; + } + + SampleValue unescapedSample(unescaped, m_Signatures->m_regexPreconditions); + checkRegex(unescapedSample, m_Signatures->specific_acuracy_keywords_regex, res.keyword_matches, + res.found_patterns, longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->words_regex, res.keyword_matches, res.found_patterns, + longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->pattern_regex, res.regex_matches, res.found_patterns, + longTextFound, binaryDataFound); + + filterKeywordsDueToLongText(res); + + // If only the filtered keywords were detected (no extras) - filter them. If any extra keyword is detected + // then leave everything + if (countNotInSet(res.keyword_matches, keywords_to_filter) == 0) { + for (const std::string &keyword_to_filter : keywords_to_filter) { + keywordsToRemove.push_back(keyword_to_filter); + } + } + + if (!binaryDataFound) { + // Recalculate repetition and/or probing indicators + unsigned int newWordsCount = 0; + calcRepetitionAndProbing(res, ignored_keywords, unescaped, detectedRepetition, detectedProbing, + newWordsCount); + // Take minimal words count because empirically it means evasion was probably succesfully decoded + wordsCount = std::min(wordsCount, newWordsCount); + } + } + + bool os_cmd_ev = Waap::Util::find_in_map_of_stringlists_keys("os_cmd_ev", res.found_patterns); + + if (os_cmd_ev) { + dbgTrace(D_WAAP_EVASIONS) << "os command evasion found"; + + // Possible os command evasion detected: - clean up and scan with regexes again. + std::string unescaped; + size_t kwCount = res.keyword_matches.size(); + size_t pos = 0; + size_t found; + + do { + found = res.unescaped_line.find('[', pos); + if (found != std::string::npos) + { + unescaped += res.unescaped_line.substr(pos, found-pos); + if (found < res.unescaped_line.size() - 3 && + res.unescaped_line[found+1] == res.unescaped_line[found+2] && res.unescaped_line[found+3] == ']') + { + unescaped += res.unescaped_line[found+1]; + pos = found+4; // [aa] + } + else + { + unescaped += res.unescaped_line[found]; + pos = found+1; + } + } + } while(found != std::string::npos); + unescaped += res.unescaped_line.substr(pos); // add tail + + if (res.unescaped_line != unescaped) { + SampleValue unescapedSample(unescaped, m_Signatures->m_regexPreconditions); + checkRegex(unescapedSample, m_Signatures->specific_acuracy_keywords_regex, res.keyword_matches, + res.found_patterns, longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->words_regex, res.keyword_matches, res.found_patterns, + longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->pattern_regex, res.regex_matches, res.found_patterns, + longTextFound, binaryDataFound); + } + + if (kwCount == res.keyword_matches.size()) { + // Remove the evasion keyword if no real evasion found + keywordsToRemove.push_back("os_cmd_ev"); + os_cmd_ev = false; + } + else if (!binaryDataFound) { + // Recalculate repetition and/or probing indicators + unsigned int newWordsCount = 0; + calcRepetitionAndProbing(res, ignored_keywords, unescaped, detectedRepetition, detectedProbing, + newWordsCount); + // Take minimal words count because empirically it means evasion was probably succesfully decoded + wordsCount = std::min(wordsCount, newWordsCount); + } + } + + bool quotes_ev = Waap::Util::find_in_map_of_stringlists_keys("quotes_ev", res.found_patterns); + + if (quotes_ev) { + dbgTrace(D_WAAP_EVASIONS) << "quotes evasion found"; + + // Possible quotes evasion detected: - clean up and scan with regexes again. + + std::string unescaped = m_Signatures->quotes_ev_pattern.sub(res.unescaped_line); + + size_t kwCount = res.keyword_matches.size(); + + if (res.unescaped_line != unescaped) { + SampleValue unescapedSample(unescaped, m_Signatures->m_regexPreconditions); + checkRegex(unescapedSample, m_Signatures->specific_acuracy_keywords_regex, res.keyword_matches, + res.found_patterns, longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->words_regex, res.keyword_matches, res.found_patterns, + longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->pattern_regex, res.regex_matches, res.found_patterns, + longTextFound, binaryDataFound); + } + + if (kwCount == res.keyword_matches.size()) { + // Remove the evasion keyword if no real evasion found + keywordsToRemove.push_back("quotes_ev"); + quotes_ev = false; + } + else if (!binaryDataFound) { + // Recalculate repetition and/or probing indicators + unsigned int newWordsCount = 0; + calcRepetitionAndProbing(res, ignored_keywords, unescaped, detectedRepetition, detectedProbing, + newWordsCount); + // Take minimal words count because empirically it means evasion was probably succesfully decoded + wordsCount = std::min(wordsCount, newWordsCount); + } + } + + if (Waap::Util::containsInvalidUtf8(line)) { + dbgTrace(D_WAAP_EVASIONS) << "invalid utf-8 evasion found"; + + // Possible quotes evasion detected: - clean up and scan with regexes again. + + std::string unescaped = Waap::Util::unescapeInvalidUtf8(line); + + size_t kwCount = res.keyword_matches.size(); + unescaped = unescape(unescaped); + + if (res.unescaped_line != unescaped) { + SampleValue unescapedSample(unescaped, m_Signatures->m_regexPreconditions); + checkRegex(unescapedSample, m_Signatures->specific_acuracy_keywords_regex, res.keyword_matches, + res.found_patterns, longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->words_regex, res.keyword_matches, res.found_patterns, + longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->pattern_regex, res.regex_matches, res.found_patterns, + longTextFound, binaryDataFound); + } + + if (kwCount != res.keyword_matches.size() && !binaryDataFound) { + // Recalculate repetition and/or probing indicators + unsigned int newWordsCount = 0; + calcRepetitionAndProbing(res, ignored_keywords, unescaped, detectedRepetition, detectedProbing, + newWordsCount); + // Take minimal words count because empirically it means evasion was probably succesfully decoded + wordsCount = std::min(wordsCount, newWordsCount); + } + } + + std::string *utf8_broken_line_ptr = nullptr; + if (Waap::Util::containsBrokenUtf8(unquote_line)) { + utf8_broken_line_ptr = &unquote_line; + } else if (Waap::Util::containsBrokenUtf8(line)) { + utf8_broken_line_ptr = (std::string*)&line; + } + + if (utf8_broken_line_ptr) { + dbgTrace(D_WAAP_EVASIONS) << "broken-down utf-8 evasion found"; + std::string unescaped = Waap::Util::unescapeBrokenUtf8(*utf8_broken_line_ptr); + size_t kwCount = res.keyword_matches.size(); + + unescaped = unescape(unescaped); + + if (res.unescaped_line != unescaped) { + SampleValue unescapedSample(unescaped, m_Signatures->m_regexPreconditions); + checkRegex(unescapedSample, m_Signatures->specific_acuracy_keywords_regex, res.keyword_matches, + res.found_patterns, longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->words_regex, res.keyword_matches, res.found_patterns, + longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->pattern_regex, res.regex_matches, res.found_patterns, + longTextFound, binaryDataFound); + } + + if (kwCount != res.keyword_matches.size() && !binaryDataFound) { + // Recalculate repetition and/or probing indicators + unsigned int newWordsCount = 0; + calcRepetitionAndProbing(res, ignored_keywords, unescaped, detectedRepetition, detectedProbing, + newWordsCount); + // Take minimal words count because empirically it means evasion was probably succesfully decoded + wordsCount = std::min(wordsCount, newWordsCount); + } + } + + bool comment_ev = Waap::Util::find_in_map_of_stringlists_keys("comment_ev", res.found_patterns); + + if (comment_ev) { + // Possible quotes evasion detected: - clean up and scan with regexes again. + dbgTrace(D_WAAP_EVASIONS) << "comment evasion found"; + + std::string unescaped = m_Signatures->comment_ev_pattern.sub(res.unescaped_line); + size_t kwCount = res.keyword_matches.size(); + + if (res.unescaped_line != unescaped) { + SampleValue unescapedSample(unescaped, m_Signatures->m_regexPreconditions); + checkRegex(unescapedSample, m_Signatures->specific_acuracy_keywords_regex, res.keyword_matches, + res.found_patterns, longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->words_regex, res.keyword_matches, res.found_patterns, + longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->pattern_regex, res.regex_matches, res.found_patterns, + longTextFound, binaryDataFound); + } + + if (kwCount == res.keyword_matches.size()) { + // Remove the evasion keyword if no real evasion found + keywordsToRemove.push_back("comment_ev"); + comment_ev = false; + } + else if (!binaryDataFound) { + // Recalculate repetition and/or probing indicators + unsigned int newWordsCount = 0; + calcRepetitionAndProbing(res, ignored_keywords, unescaped, detectedRepetition, detectedProbing, + newWordsCount); + // Take minimal words count because empirically it means evasion was probably succesfully decoded + wordsCount = std::min(wordsCount, newWordsCount); + } + } + + bool quoutes_space_evasion = Waap::Util::find_in_map_of_stringlists_keys( + "quotes_space_ev_fast_reg", + res.found_patterns + ); + + if (quoutes_space_evasion) { + // Possible quotes space evasion detected: - clean up and scan with regexes again. + dbgTrace(D_WAAP_EVASIONS) << "quotes space evasion found"; + std::string unescaped = m_Signatures->quotes_space_ev_pattern.sub(res.unescaped_line); + size_t kwCount = res.keyword_matches.size(); + + if (res.unescaped_line != unescaped) { + SampleValue unescapedSample(unescaped, m_Signatures->m_regexPreconditions); + checkRegex(unescapedSample, m_Signatures->specific_acuracy_keywords_regex, res.keyword_matches, + res.found_patterns, longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->words_regex, res.keyword_matches, res.found_patterns, + longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->pattern_regex, res.regex_matches, res.found_patterns, + longTextFound, binaryDataFound); + } + + if (kwCount == res.keyword_matches.size()) { + // Remove the evasion keyword if no real evasion found + keywordsToRemove.push_back("quotes_space_evasion"); + quoutes_space_evasion = false; + } + else if (!binaryDataFound) { + // Recalculate repetition and/or probing indicators + unsigned int newWordsCount = 0; + calcRepetitionAndProbing(res, ignored_keywords, unescaped, detectedRepetition, detectedProbing, + newWordsCount); + // Take minimal words count because empirically it means evasion was probably succesfully decoded + wordsCount = std::min(wordsCount, newWordsCount); + } + } + + if (Waap::Util::testUrlBareUtf8Evasion(line)) { + // Possible quotes evasion detected: - clean up and scan with regexes again. + dbgTrace(D_WAAP_EVASIONS) << "url_bare_utf8 evasion found"; + + // Revert the encoding and rescan again + // Insert additional '%' character after each sequence of three characters either "%C0" or "%c0". + std::string unescaped = line; + replaceAll(unescaped, "%c0", "%c0%"); + replaceAll(unescaped, "%C0", "%C0%"); + + // Run the result through another pass of "unescape" which will now correctly urldecode and utf8-decode it + unescaped = unescape(unescaped); + size_t kwCount = res.keyword_matches.size(); + + if (res.unescaped_line != unescaped) { + SampleValue unescapedSample(unescaped, m_Signatures->m_regexPreconditions); + checkRegex(unescapedSample, m_Signatures->specific_acuracy_keywords_regex, res.keyword_matches, + res.found_patterns, longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->words_regex, res.keyword_matches, res.found_patterns, + longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->pattern_regex, res.regex_matches, res.found_patterns, + longTextFound, binaryDataFound); + } + + if (kwCount != res.keyword_matches.size() && !binaryDataFound) { + // Recalculate repetition and/or probing indicators + unsigned int newWordsCount = 0; + calcRepetitionAndProbing(res, ignored_keywords, unescaped, detectedRepetition, detectedProbing, + newWordsCount); + // Take minimal words count because empirically it means evasion was probably succesfully decoded + wordsCount = std::min(wordsCount, newWordsCount); + } + } + + if ((res.unescaped_line.find("0x") != std::string::npos) && evasion_hex_regex.hasMatch(res.unescaped_line)) { + dbgTrace(D_WAAP_EVASIONS) << "hex evasion found (in unescaped line)"; + + std::string unescaped = res.unescaped_line; + replaceAll(unescaped, "0x", "\\x"); + unescapeUnicode(unescaped); + dbgTrace(D_WAAP_EVASIONS) << "unescaped =='" << unescaped << "'"; + + size_t kwCount = res.keyword_matches.size(); + + if (res.unescaped_line != unescaped) { + SampleValue unescapedSample(unescaped, m_Signatures->m_regexPreconditions); + checkRegex(unescapedSample, m_Signatures->specific_acuracy_keywords_regex, res.keyword_matches, + res.found_patterns, false, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->words_regex, res.keyword_matches, res.found_patterns, + false, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->pattern_regex, res.regex_matches, res.found_patterns, + false, binaryDataFound); + } + + if (kwCount != res.keyword_matches.size() && !binaryDataFound) { + for (const auto &kw : res.keyword_matches) { + if (kw.size() < 2 || str_contains(kw, "os_cmd_high_acuracy_fast_reg") || + str_contains(kw, "regex_code_execution") || kw == "character_encoding" || + str_contains(kw, "quotes_ev_fast_reg") || str_contains(kw, "encoded_") || + str_contains(kw, "medium_acuracy") || str_contains(kw, "high_acuracy_fast_reg_xss")) + { + keywordsToRemove.push_back(kw); + } + } + + // Recalculate repetition and/or probing indicators + unsigned int newWordsCount = 0; + calcRepetitionAndProbing(res, ignored_keywords, unescaped, detectedRepetition, detectedProbing, + newWordsCount); + // Take minimal words count because empirically it means evasion was probably succesfully decoded + wordsCount = std::min(wordsCount, newWordsCount); + } + + } + + if ((line.find("0x") != std::string::npos) && evasion_hex_regex.hasMatch(line)) { + dbgTrace(D_WAAP_EVASIONS) << "hex evasion found (in raw line)"; + std::string unescaped = line; + replaceAll(unescaped, "0x", "\\x"); + unescapeUnicode(unescaped); + dbgTrace(D_WAAP_EVASIONS) << "unescape == '" << unescaped << "'"; + + size_t kwCount = res.keyword_matches.size(); + + if (res.unescaped_line != unescaped) { + SampleValue unescapedSample(unescaped, m_Signatures->m_regexPreconditions); + checkRegex(unescapedSample, m_Signatures->specific_acuracy_keywords_regex, res.keyword_matches, + res.found_patterns, false, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->words_regex, res.keyword_matches, res.found_patterns, + false, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->pattern_regex, res.regex_matches, res.found_patterns, + false, binaryDataFound); + } + + if (kwCount != res.keyword_matches.size() && !binaryDataFound) { + for (const auto &kw : res.keyword_matches) { + if (kw.size() < 2 || str_contains(kw, "os_cmd_high_acuracy_fast_reg") || + str_contains(kw, "regex_code_execution") || kw == "character_encoding" || + str_contains(kw, "quotes_ev_fast_reg") || str_contains(kw, "encoded_") || + str_contains(kw, "medium_acuracy") || str_contains(kw, "high_acuracy_fast_reg_xss")) + { + keywordsToRemove.push_back(kw); + } + } + // Recalculate repetition and/or probing indicators + unsigned int newWordsCount = 0; + calcRepetitionAndProbing(res, ignored_keywords, unescaped, detectedRepetition, detectedProbing, + newWordsCount); + // Take minimal words count because empirically it means evasion was probably succesfully decoded + wordsCount = std::min(wordsCount, newWordsCount); + } + + } + + if ((res.unescaped_line.find("%") != std::string::npos) && evasion_bad_hex_regex.hasMatch(res.unescaped_line)) { + dbgTrace(D_WAAP_EVASIONS) << "Bad hex evasion found (%c1%1c or %c1%9c in unescaped line)"; + + std::string unescaped = res.unescaped_line; + + unescaped = boost::regex_replace(unescaped, bad_hex_regex, "/"); + unescaped = unescape(unescaped); + dbgTrace(D_WAAP_EVASIONS) << "unescaped =='" << unescaped << "'"; + + size_t kwCount = res.keyword_matches.size(); + + if (res.unescaped_line != unescaped) { + SampleValue unescapedSample(unescaped, m_Signatures->m_regexPreconditions); + checkRegex(unescapedSample, m_Signatures->specific_acuracy_keywords_regex, res.keyword_matches, + res.found_patterns, longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->words_regex, res.keyword_matches, res.found_patterns, + longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->pattern_regex, res.regex_matches, res.found_patterns, + longTextFound, binaryDataFound); + } + + if (kwCount != res.keyword_matches.size() && !binaryDataFound) { + // Recalculate repetition and/or probing indicators + unsigned int newWordsCount = 0; + calcRepetitionAndProbing(res, ignored_keywords, unescaped, detectedRepetition, detectedProbing, + newWordsCount); + // Take minimal words count because empirically it means evasion was probably succesfully decoded + wordsCount = std::min(wordsCount, newWordsCount); + } + + } + + if ((line.find("%") != std::string::npos) && evasion_bad_hex_regex.hasMatch(line)) { + dbgTrace(D_WAAP_EVASIONS) << "Bad hex evasion found (%c1%1c or %c1%9c in raw line)"; + std::string unescaped = line; + + unescaped = boost::regex_replace(unescaped, bad_hex_regex, "/"); + unescaped = unescape(unescaped); + dbgTrace(D_WAAP_EVASIONS) << "unescaped == '" << unescaped << "'"; + + size_t kwCount = res.keyword_matches.size(); + + if (res.unescaped_line != unescaped) { + SampleValue unescapedSample(unescaped, m_Signatures->m_regexPreconditions); + checkRegex(unescapedSample, m_Signatures->specific_acuracy_keywords_regex, res.keyword_matches, + res.found_patterns, longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->words_regex, res.keyword_matches, res.found_patterns, + longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->pattern_regex, res.regex_matches, res.found_patterns, + longTextFound, binaryDataFound); + } + + if (kwCount != res.keyword_matches.size() && !binaryDataFound) { + // Recalculate repetition and/or probing indicators + unsigned int newWordsCount = 0; + calcRepetitionAndProbing(res, ignored_keywords, unescaped, detectedRepetition, detectedProbing, + newWordsCount); + // Take minimal words count because empirically it means evasion was probably succesfully decoded + wordsCount = std::min(wordsCount, newWordsCount); + } + } + + // python: escape ='hi_acur_fast_reg_evasion' in found_patterns + bool escape = Waap::Util::find_in_map_of_stringlists_keys("evasion", res.found_patterns); + + if (escape) { + // Possible evasion detected: remove unicode \u and \x sequences, + // delete all trash in un_escape_pattern, and scan with regexes again. + dbgTrace(D_WAAP_EVASIONS) << "escape pattern found"; + + std::string unescaped = res.unescaped_line; + + dbgTrace(D_WAAP_EVASIONS) << "unescape'" << unescaped << "'"; + replaceAll(unescaped, "0x", "\\x"); + replaceAll(unescaped, "%u", "\\u"); + std::string zero; + zero.push_back(0); + replaceAll(unescaped, zero, ""); + unescapeUnicode(unescaped); + + // from python: unescaped = un_escape_pattern.sub(r'',line) + ' ' + un_escape_pattern.sub(r' ',line) + // note: "line" in python is called "unescaped" in this code. + unescaped = m_Signatures->un_escape_pattern.sub(unescaped) + " " + + m_Signatures->un_escape_pattern.sub(unescaped, " "); + + size_t kwCount = res.keyword_matches.size(); + + if (res.unescaped_line != unescaped) { + SampleValue unescapedSample(unescaped, m_Signatures->m_regexPreconditions); + checkRegex(unescapedSample, m_Signatures->specific_acuracy_keywords_regex, res.keyword_matches, + res.found_patterns, longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->words_regex, res.keyword_matches, res.found_patterns, + longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->pattern_regex, res.regex_matches, res.found_patterns, + longTextFound, binaryDataFound); + } + + if (kwCount == res.keyword_matches.size()) { + // Remove the evasion keyword if no real evasion found + keywordsToRemove.push_back("evasion"); + escape = false; + } + else if (!binaryDataFound) { + // Recalculate repetition and/or probing indicators + unsigned int newWordsCount = 0; + calcRepetitionAndProbing(res, ignored_keywords, unescaped, detectedRepetition, detectedProbing, + newWordsCount); + // Take minimal words count because empirically it means evasion was probably succesfully decoded + wordsCount = std::min(wordsCount, newWordsCount); + } + } + + // Detect bash "backslash" evasions + // Note that the search for low binary ASCII codes such as 7 or 8 are done here because + // unescaped_line after unescape() contains post-processed string, where original \b was already converted to + // single character (ASCII 8). + // This should handle cases like /\bin/sh + unsigned char prev_uch = '\0'; + for (char ch : res.unescaped_line) { + unsigned char uch = (unsigned char)ch; + if ((uch >= 0x07 && uch <= 0x0D) || (uch == '\\') || (uch == '/' && prev_uch == '/')) { + escape = true; + break; + } + prev_uch = uch; + } + + if (escape) { + dbgTrace(D_WAAP_EVASIONS) << "try decoding bash evasions"; + + // Possible bash evasion detected: - clean up and scan with regexes again. + dbgTrace(D_WAAP_EVASIONS) << "unescape='" << res.unescaped_line << "'"; + + std::string unescaped; + unescaped.reserve(res.unescaped_line.size()); // preallocate to improve performance of += clauses below + + // Partially revert the effect of the escape_backslashes() function, remove the '\' characters and + // squash string of successive forward slashes to single slash. + // This allows us to decode bash evasions like "/\b\i\n/////s\h" + char prev_ch = '\0'; + for (char ch : res.unescaped_line) { + switch (ch) { + case 7: unescaped += "a"; break; + case 8: unescaped += "b"; break; + case 9: unescaped += "t"; break; + case 10: unescaped += "n"; break; + case 11: unescaped += "v"; break; + case 12: unescaped += "f"; break; + case 13: unescaped += "r"; break; + case '\\': break; // remove backslashes + default: + // squash strings of successive '/' characters into single '/' character + if (prev_ch == '/' && ch == '/') { + break; + } + unescaped += ch; + } + + prev_ch = ch; + } + + size_t kwCount = res.keyword_matches.size(); + + if (res.unescaped_line != unescaped) { + SampleValue unescapedSample(unescaped, m_Signatures->m_regexPreconditions); + checkRegex(unescapedSample, m_Signatures->specific_acuracy_keywords_regex, res.keyword_matches, + res.found_patterns, longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->words_regex, res.keyword_matches, res.found_patterns, + longTextFound, binaryDataFound); + checkRegex(unescapedSample, m_Signatures->pattern_regex, res.regex_matches, res.found_patterns, + longTextFound, binaryDataFound); + } + + if (kwCount == res.keyword_matches.size()) { + // Remove the evasion keyword if no real evasion found + keywordsToRemove.push_back("evasion"); + escape = false; + } + else if (!binaryDataFound) { + // Recalculate repetition and/or probing indicators + unsigned int newWordsCount = 0; + calcRepetitionAndProbing(res, ignored_keywords, unescaped, detectedRepetition, detectedProbing, + newWordsCount); + // Take minimal words count because empirically it means evasion was probably succesfully decoded + wordsCount = std::min(wordsCount, newWordsCount); + } + } + + // Remove evasion keywords that should not be reported because there's no real evasion found + if (!keywordsToRemove.empty()) { + dbgTrace(D_WAAP_SAMPLE_SCAN) + << "Removing these keywords (probably due to evasions): " + << Waap::Util::vecToString(keywordsToRemove); + } + + for (const auto &value : keywordsToRemove) { + Waap::Util::remove_startswith(res.keyword_matches, value); + Waap::Util::remove_in_map_of_stringlists_keys(value, res.found_patterns); + } + + + // python: + // if headers: + // keyword_matches = [x for x in keyword_matches if x not in '\(/);$='] + if (isHeaderScanStage) { + removeItemsMatchingSubstringOf(res.keyword_matches, "\\(/);$="); + // For headers, also remove all ignored patterns entirely, not just ignore it from counts + for (const auto &ignored_pattern : *ignored_patterns) { + if (res.found_patterns.erase(ignored_pattern)) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "Removed the found pattern in header: '" << ignored_pattern << "'"; + } + } + } + + // python: + // keywords_num = sum(1 for x in keyword_matches if x not in ignored_keywords) + size_t keywords_num = countNotInSet(res.keyword_matches, *ignored_keywords); + size_t regex_num = countNotInSet(res.regex_matches, *ignored_patterns); + + bool forceReport = isUrlScanStage && Waap::Util::find_in_map_of_stringlists_keys("url", res.found_patterns); + + if (forceReport) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "setting forceReport becacuse we are in url context and " + "'high_acuracy_fast_reg_evation' pattern is found!"; + } + + // python: + // if keywords_num >2 or ('acuracy' in patterns and not headers) or + // special_patten in patterns or 'probing' in keyword_matches or 'repetition' in keyword_matches: + if (keywords_num + regex_num > 2 || + Waap::Util::find_in_map_of_stringlists_keys("acur", res.found_patterns) || + forceReport || + detectedRepetition || + detectedProbing) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "pre-suspicion found."; + // apply regex signatures + checkRegex(unescapedLineSample, m_Signatures->pattern_regex, res.regex_matches, res.found_patterns, + longTextFound, binaryDataFound); + + // python: + // if len(regex_matches) and 'probing' not in keyword_matches: + // if len(keyword_matches+regex_matches)+2>words: + // keyword_matches.append('probing') + if (!binaryDataFound && res.regex_matches.size() > 0 && !detectedProbing) { + // if len(''.join(res.keyword_matches+res.regex_matches))>=alphanumeric_num { + if (res.keyword_matches.size() + res.regex_matches.size() + 2 > wordsCount) { + detectedProbing = true; + res.keyword_matches.push_back("probing"); + } + } + + // python: + // keywords_num = sum(1 for x in keyword_matches if x not in ignored_keywords) + keywords_num = countNotInSet(res.keyword_matches, *ignored_keywords); + regex_num = countNotInSet(res.regex_matches, *ignored_patterns); + + // Regular (medium) acuracy contributes 1 to the score. + // High acuracy contributes 2 to the score. + int acuracy = 0; + + // python: + // if 'acuracy' in patterns and not url: + if (Waap::Util::find_in_map_of_stringlists_keys("acur", res.found_patterns)) + { + acuracy = 1; + // search for "high_acuracy" or "hi_acur" signature names + if (Waap::Util::find_in_map_of_stringlists_keys("high", res.found_patterns) || + Waap::Util::find_in_map_of_stringlists_keys("hi_acur", res.found_patterns)) + { + acuracy = 2; + } + } + + // "Acuracy" contribution alone won't trigger suspicion yet. It needs additional boost + // of finding some keywords and/or matched regexes. + int score = keywords_num + acuracy + (2 * regex_num); + +#ifdef WAF2_LOGGING_ENABLE + // Dump interesting statistics and scores + print_filtered("keywords", *ignored_keywords, res.keyword_matches); + print_filtered("patterns", *ignored_patterns, res.regex_matches); + print_found_patterns(res.found_patterns); + + dbgTrace(D_WAAP_SAMPLE_SCAN) << "before decision: keywords(num=" << keywords_num << ", size=" << + res.keyword_matches.size() << "); regex(num=" << regex_num << ", size=" << res.regex_matches.size() << + "; acuracy=" << acuracy << "; score=" << score << "; forceReport=" << forceReport << "; probing=" << + detectedProbing << "; repetition=" << detectedRepetition << "; 'fast_reg' in found_patterns: " << + Waap::Util::find_in_map_of_stringlists_keys("fast_reg", res.found_patterns); +#endif + + // python: + // if (keywords_num+acuracy+2*regex_num)>2 or special_patten in patterns or + // 'fast_reg' in patterns or 'probing' in keyword_matches or 'repetition' in keyword_matches: + if (score > 2 || + forceReport || + detectedProbing || + detectedRepetition || + Waap::Util::find_in_map_of_stringlists_keys("fast_reg", res.found_patterns)) { + dbgTrace(D_WAAP_SAMPLE_SCAN) << "apply(): suspicion found (score=" << score << ")."; + + if (shouldCache) { + m_suspiciousValuesCache.insert( + {CacheKey(line, scanStage, isBinaryData, splitType.ok() ? *splitType : ""), res} + ); + } + + return true; // suspicion found + } + + dbgTrace(D_WAAP_SAMPLE_SCAN) << "apply(): suspicion not found (score=" << score << ")."; + } + + dbgTrace(D_WAAP_SAMPLE_SCAN) << "apply(): not suspicious."; + + if (shouldCache) { + m_cleanValuesCache.insert(CacheKey(line, scanStage, isBinaryData, splitType.ok() ? *splitType : "")); + } + + res.clear(); + return false; +} + +void WaapAssetState::updateScores() +{ + scoreBuilder.snap(); +} + +std::string WaapAssetState::getSignaturesScoresFilePath() const { + return m_SignaturesScoresFilePath; +} + +std::map>& WaapAssetState::getFilterVerbose() +{ + return m_filtered_keywords_verbose; +} + +std::string WaapAssetState::getSignaturesFilterDir() const { + size_t lastSlash = m_SignaturesScoresFilePath.find_last_of('/'); + std::string sigsFilterDir = ((lastSlash == std::string::npos) ? + m_SignaturesScoresFilePath : m_SignaturesScoresFilePath.substr(0, lastSlash)); + dbgTrace(D_WAAP_ASSET_STATE) << " signatures filters directory: " << sigsFilterDir; + return sigsFilterDir; +} + +void WaapAssetState::updateFilterManagerPolicy(IWaapConfig* pConfig) +{ + m_filtersMngr->loadPolicy(pConfig); +} + +bool WaapAssetState::isKeywordOfType(const std::string& keyword, ParamType type) const +{ + return m_typeValidator.isKeywordOfType(keyword, type); +} + +bool WaapAssetState::isBinarySampleType(const std::string & sample) const +{ + // Binary data detection is based on existance of at least two ASCII NUL bytes + size_t nulBytePos = sample.find('\0', 0); + if (nulBytePos != std::string::npos) { + nulBytePos = sample.find('\0', nulBytePos+1); + if (nulBytePos != std::string::npos) { + dbgTrace(D_WAAP_ASSET_STATE) << "binary_input sample type detected (nul bytes)"; + return true; + } + } + + std::vector matches; + m_Signatures->format_magic_binary_re.findAllMatches(sample, matches); + if (!matches.empty()) { + dbgTrace(D_WAAP_ASSET_STATE) << "binary_input sample type detected (signature)"; + return true; + } + + return false; +} + +static Maybe +parse_wbxml_uint8(const std::string & sample, size_t &offset) +{ + if (offset >= sample.size()) { + return genError("not wbxml"); + } + return sample[offset++]; +} + +static Maybe +parse_wbxml_mb_uint32(const std::string & sample, size_t &offset) +{ + uint32_t value = 0; + for (int i=0; i < 5; i++) { + Maybe v = parse_wbxml_uint8(sample, offset); + if (!v.ok()) return genError("not wbxml"); + value = (value << 7) | (*v & 0x7F); + if ((*v & 0x80) == 0) { + return value; + } + } + return genError("not wbxml"); +} + +bool WaapAssetState::isWBXMLSampleType(const std::string & sample) const +{ + size_t offset = 0; + // Parse protocol version + Maybe version = parse_wbxml_uint8(sample, offset); + // Support only wbxml protocol versions 1-3 which can be more or less reliably detected + if (!version.ok() || *version==0 || *version > 0x03) return false; + // Parse public id + Maybe public_id = parse_wbxml_mb_uint32(sample, offset); + if (!public_id.ok()) return false; + // Parse and validate charset (this is optional for v0 but we don't detect v0 anyway) + Maybe charset = parse_wbxml_mb_uint32(sample, offset); + if (!charset.ok()) return false; + // Only subset of charsets are allowed + static const uint32_t allowed_charsets[] = {0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 17, 106, 1000, 1015, 2026}; + if (std::find(std::begin(allowed_charsets), std::end(allowed_charsets), *charset) == + std::end(allowed_charsets)) + { + return false; + } + Maybe strtbl_len = parse_wbxml_mb_uint32(sample, offset); + return strtbl_len.ok() && *strtbl_len <= sample.size() - offset; +} + +std::set WaapAssetState::getSampleType(const std::string & sample) const +{ + std::set types; + bool shouldCache = (sample.size() <= MAX_CACHE_VALUE_SIZE); + + // Handle cached clean values + if (shouldCache && m_sampleTypeCache.exist(sample)) { + dbgTrace(D_WAAP_ASSET_STATE) << "WaapAssetState::getSampleType() sample: '" << sample << + "' type is unknown (cache)"; + types.insert("unknown"); + return types; + } + + for (auto& type_re : m_Signatures->params_type_re) + { + dbgTrace(D_WAAP_ASSET_STATE) << "WaapAssetState::getSampleType checking: " << sample << + " against " << type_re.first; + std::vector matches; + type_re.second->findAllMatches(sample, matches); + + dbgTrace(D_WAAP_ASSET_STATE) << "number of matched keywords: " << matches.size(); + if (matches.empty()) + { + continue; + } + + types.insert(type_re.first); + } + + // Binary data detection is based on existance of at least two ASCII NUL bytes + if (isBinarySampleType(sample)) { + dbgTrace(D_WAAP_ASSET_STATE) << "reporting binary_input sample type"; + types.insert("binary_input"); + } + + if (types.empty()) + { + types.insert("unknown"); + m_sampleTypeCache.insert(sample); + } + + return types; +} + +void WaapAssetState::logIndicatorsInFilters(const std::string ¶m, Waap::Keywords::KeywordsSet& keywords, + IWaf2Transaction* pTransaction) +{ + m_filtersMngr->registerKeywords(param, keywords, pTransaction); +} + +void WaapAssetState::logParamHit(Waf2ScanResult& res, IWaf2Transaction* pTransaction) +{ + Waap::Keywords::KeywordsSet emptySet; + std::string key = IndicatorsFiltersManager::generateKey(res.location, res.param_name, pTransaction); + m_filtersMngr->registerKeywords(key, emptySet, pTransaction); +} + +void WaapAssetState::filterKeywords( + const std::string ¶m, + Waap::Keywords::KeywordsSet& keywords, + std::vector& filteredKeywords) +{ + dbgTrace(D_WAAP_ASSET_STATE) << "filter keywords"; + m_filtersMngr->filterKeywords(param, keywords, filteredKeywords); +} + +void WaapAssetState::clearFilterVerbose() +{ + m_filtered_keywords_verbose.clear(); +} + +void WaapAssetState::filterVerbose(const std::string ¶m, + std::vector& filteredKeywords) +{ + m_filtersMngr->filterVerbose(param, filteredKeywords, m_filtered_keywords_verbose); +} + +void WaapAssetState::filterKeywordsByParameters( + const std::string ¶meter_name, Waap::Keywords::KeywordsSet &keywords_set) +{ + dbgTrace(D_WAAP_ASSET_STATE) << "filter keywords based on parameter name: " << parameter_name; + auto filter_parameters_itr = m_Signatures->filter_parameters.find(parameter_name); + if (filter_parameters_itr != m_Signatures->filter_parameters.end()) + { + dbgTrace(D_WAAP_ASSET_STATE) << "Found keywords to filter based on parameter name"; + const auto &vec = filter_parameters_itr->second; + for (auto keyword_to_filter : vec) + { + auto keywords_set_itr = keywords_set.find(keyword_to_filter); + if (keywords_set_itr != keywords_set.end()) + { + dbgTrace(D_WAAP_ASSET_STATE) << "Filtering keyword: " << keyword_to_filter; + keywords_set.erase(keyword_to_filter); + } + } + } + else + { + dbgTrace(D_WAAP_ASSET_STATE) << "No keywords need to be filter for this parameter"; + } +} + +void WaapAssetState::removeKeywords(Waap::Keywords::KeywordsSet &keywords_set) +{ + for (auto &keyword_to_remove : m_Signatures->remove_keywords_always) + { + auto keyword_set_itr = keywords_set.find(keyword_to_remove); + if (keyword_set_itr != keywords_set.end()) + { + dbgTrace(D_WAAP_ASSET_STATE) << "Removing keyword: " << keyword_to_remove << " from keyword set"; + keywords_set.erase(keyword_set_itr); + } + } +} + +void WaapAssetState::removeWBXMLKeywords(Waap::Keywords::KeywordsSet &keywords_set, + std::vector &filtered_keywords) +{ + for (auto it = keywords_set.begin(); it != keywords_set.end();) { + if (NGEN::Regex::regexMatch(__FILE__, __LINE__, *it, m_Signatures->wbxml_data_kw_filter)) { + dbgTrace(D_WAAP_ASSET_STATE) << "Filtering keyword due to wbxml: '" << *it << "'"; + filtered_keywords.push_back(*it); + it = keywords_set.erase(it); + } + else { + ++it; + } + } +} + +void WaapAssetState::createRateLimitingState(const std::shared_ptr &rateLimitingPolicy) +{ + m_rateLimitingState = std::make_shared(rateLimitingPolicy); +} + +void WaapAssetState::createErrorLimitingState(const std::shared_ptr &errorLimitingPolicy) +{ + m_errorLimitingState = std::make_shared(errorLimitingPolicy); +} + +void WaapAssetState::createSecurityHeadersState( + const std::shared_ptr &securityHeadersPolicy) +{ + m_securityHeadersState = std::make_shared(securityHeadersPolicy); +} + +std::shared_ptr& WaapAssetState::getRateLimitingState() +{ + return m_rateLimitingState; +} + +std::shared_ptr& WaapAssetState::getErrorLimitingState() +{ + return m_errorLimitingState; +} + +std::shared_ptr& WaapAssetState::getSecurityHeadersState() +{ + return m_securityHeadersState; +} + +void WaapAssetState::clearRateLimitingState() +{ + m_rateLimitingState.reset(); +} + +void WaapAssetState::clearErrorLimitingState() +{ + m_errorLimitingState.reset(); +} + +void WaapAssetState::clearSecurityHeadersState() +{ + m_securityHeadersState.reset(); +} + diff --git a/components/security_apps/waap/waap_clib/WaapAssetState.h b/components/security_apps/waap/waap_clib/WaapAssetState.h new file mode 100755 index 0000000..362dc54 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapAssetState.h @@ -0,0 +1,171 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __WAF2_SIGS_H__02a5bdaa +#define __WAF2_SIGS_H__02a5bdaa + +#include "Waf2Regex.h" +#include "Signatures.h" +#include "picojson.h" +#include "lru_cache_set.h" +#include "lru_cache_map.h" +#include +#include +#include +#include +#include "ScoreBuilder.h" +#include "i_encryptor.h" +#include "i_waap_asset_state.h" +#include "RateLimiting.h" +#include "SecurityHeadersPolicy.h" +#include "WaapDefines.h" +#include "IndicatorsFiltersManager.h" +#include "WaapKeywords.h" +#include "KeywordTypeValidator.h" +#include "ScanResult.h" +#include "WaapSampleValue.h" + +class IWaf2Transaction; + +class WaapAssetState : public boost::noncopyable, public I_WaapAssetState +{ +private: //ugly but needed for build + std::shared_ptr m_Signatures; + std::string m_SignaturesScoresFilePath; + std::map> m_filtered_keywords_verbose; + + void checkRegex(const SampleValue &sample, const Regex & pattern, std::vector& keyword_matches, + Waap::Util::map_of_stringlists_t & found_patterns, bool longTextFound, bool binaryDataFound) const; + + void filterKeywordsDueToLongText(Waf2ScanResult &res) const; + +public: + // Load and compile signatures from file + explicit WaapAssetState(std::shared_ptr signatures, const std::string& sigScoresFname, + size_t cleanCacheCapacity = SIGS_APPLY_CLEAN_CACHE_CAPACITY, + size_t suspiciousCacheCapacity = SIGS_APPLY_SUSPICIOUS_CACHE_CAPACITY, + size_t sampleTypeCacheCapacity = SIGS_SAMPLE_TYPE_CACHE_CAPACITY, + const std::string& assetId = ""); + explicit WaapAssetState(const std::shared_ptr& pWaapAssetState, const std::string& sigScoresFname, + const std::string& assetId); + virtual ~WaapAssetState(); + + std::shared_ptr getSignatures() const; + void reset(); + + const std::string m_assetId; + + ScoreBuilder scoreBuilder; + std::shared_ptr m_rateLimitingState; + std::shared_ptr m_errorLimitingState; + std::shared_ptr m_securityHeadersState; + std::shared_ptr m_filtersMngr; + KeywordTypeValidator m_typeValidator; + + bool apply(const std::string &v, Waf2ScanResult &res, const std::string &scanStage, bool isBinaryData=false, + const Maybe splitType=genError("not splitted")) const; + + virtual void updateScores(); + virtual std::string getSignaturesScoresFilePath() const; + virtual std::string getSignaturesFilterDir() const; + std::map>& getFilterVerbose(); + + void updateFilterManagerPolicy(IWaapConfig* pConfig); + virtual bool isKeywordOfType(const std::string& keyword, ParamType type) const; + virtual bool isBinarySampleType(const std::string& sample) const; + virtual bool isWBXMLSampleType(const std::string &sample) const; + virtual std::set getSampleType(const std::string& sample) const; + void logIndicatorsInFilters(const std::string ¶m, Waap::Keywords::KeywordsSet& keywords, + IWaf2Transaction* pTransaction); + void logParamHit(Waf2ScanResult& res, IWaf2Transaction* pTransaction); + void filterKeywords(const std::string ¶m, Waap::Keywords::KeywordsSet& keywords, + std::vector& filteredKeywords); + void clearFilterVerbose(); + void filterVerbose(const std::string ¶m, + std::vector& filteredKeywords); + void filterKeywordsByParameters(const std::string ¶meter_name, Waap::Keywords::KeywordsSet &keywords_set); + void removeKeywords(Waap::Keywords::KeywordsSet &keywords_set); + void removeWBXMLKeywords(Waap::Keywords::KeywordsSet &keywords_set, std::vector &filtered_keywords); + + void createRateLimitingState(const std::shared_ptr &rateLimitingPolicy); + void createErrorLimitingState(const std::shared_ptr &errorLimitingPolicy); + void createSecurityHeadersState(const std::shared_ptr &securityHeadersPolicy); + + void clearRateLimitingState(); + void clearErrorLimitingState(); + void clearSecurityHeadersState(); + + std::shared_ptr& getRateLimitingState(); + std::shared_ptr& getErrorLimitingState(); + std::shared_ptr& getSecurityHeadersState(); + + // Key for the caches includes input values passed to the WaapAssetState::apply() + struct CacheKey { + std::string line; + std::string scanStage; + bool isBinaryData; + std::string splitType; + CacheKey( + const std::string &line, + const std::string &scanStage, + bool isBinaryData, + const std::string &splitType) + : + line(line), + scanStage(scanStage), + isBinaryData(isBinaryData), + splitType(splitType) + { + } + + // comparison operator should be implemented to use this struct as a key in an LRU cache. + bool operator==(CacheKey const& other) const + { + return + line == other.line && + scanStage == other.scanStage && + isBinaryData == other.isBinaryData && + splitType == other.splitType; + } + }; + + // LRU caches are used to increase performance of apply() method for most frequent values + mutable LruCacheSet m_cleanValuesCache; + mutable LruCacheMap m_suspiciousValuesCache; + mutable LruCacheSet m_sampleTypeCache; +}; + +// Support efficient hashing for the CacheKey struct so it can participate in unordered (hashed) containers +inline std::size_t hash_value(WaapAssetState::CacheKey const &cacheKey) +{ + std::size_t hash = 0; + boost::hash_combine(hash, cacheKey.line); + boost::hash_combine(hash, cacheKey.scanStage); + return hash; +} + +void filterUnicode(std::string & text); +void replaceUnicodeSequence(std::string & text, const char repl); +std::string unescape(const std::string & s); + +// This if function is exposed to be tested by unit tests +void +checkRegex( + std::string line, + const Regex &pattern, + std::vector& keyword_matches, + std::vector& keyword_matches_raw, + Waap::Util::map_of_stringlists_t &found_patterns, + bool longTextFound); + +#endif // __WAF2_SIGS_H__02a5bdaa diff --git a/components/security_apps/waap/waap_clib/WaapAssetStatesManager.cc b/components/security_apps/waap/waap_clib/WaapAssetStatesManager.cc new file mode 100755 index 0000000..8819f1e --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapAssetStatesManager.cc @@ -0,0 +1,187 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapAssetStatesManager.h" +#include "WaapDefines.h" +#include "WaapAssetState.h" +#include "i_waapConfig.h" +#include "config.h" +#include "agent_core_utilities.h" + +USE_DEBUG_FLAG(D_WAAP); + +WaapAssetStatesManager::WaapAssetStatesManager() : pimpl(std::make_unique()) +{ +} + +WaapAssetStatesManager::~WaapAssetStatesManager() +{ +} + +void WaapAssetStatesManager::preload() +{ + registerExpectedConfiguration("waap data", "base folder"); +} + +bool WaapAssetStatesManager::initBasicWaapSigs(const std::string& sigsFname, const std::string& sigScoresFname) +{ + return pimpl->initBasicWaapSigs(sigsFname, sigScoresFname); +} + +std::shared_ptr WaapAssetStatesManager::getWaapAssetStateGlobal() +{ + return pimpl->getWaapAssetStateGlobal(); +} + +std::shared_ptr WaapAssetStatesManager::getWaapAssetStateById(const std::string& assetId) +{ + return pimpl->getWaapAssetStateById(assetId); +} + +void WaapAssetStatesManager::setAssetDirectoryPath(const std::string &assetDirectoryPath) +{ + return pimpl->setAssetDirectoryPath(assetDirectoryPath); +} + +WaapAssetStatesManager::Impl::Impl() : + m_signatures(nullptr), + m_basicWaapSigs(nullptr), + m_AssetBasedWaapSigs(), + m_assetDirectoryPath(BACKUP_DIRECTORY_PATH) +{ +} + +WaapAssetStatesManager::Impl::~Impl() +{ +} + +bool WaapAssetStatesManager::Impl::initBasicWaapSigs(const std::string& sigsFname, const std::string& sigScoresFname) +{ + if (m_signatures && !m_signatures->fail() && m_basicWaapSigs) + { + // already initialized successfully. + return true; + } + try { + m_signatures = std::make_shared(sigsFname); + m_basicWaapSigs = std::make_shared( + m_signatures, + sigScoresFname, + SIGS_APPLY_CLEAN_CACHE_CAPACITY, + SIGS_APPLY_SUSPICIOUS_CACHE_CAPACITY); + } + catch (std::runtime_error & e) { + // TODO:: properly handle component initialization failure + dbgTrace(D_WAAP) << + "WaapAssetStatesManager::initBasicWaapSigs(): " << e.what() << ". Failed to read signature files" + " "<< sigsFname << " and " << sigScoresFname << "."; + m_basicWaapSigs.reset(); + return false; + } + + return m_signatures && !m_signatures->fail() && m_basicWaapSigs; +} + +std::shared_ptr WaapAssetStatesManager::Impl::getWaapAssetStateGlobal() +{ + return m_basicWaapSigs; +} + +std::shared_ptr WaapAssetStatesManager::Impl::getWaapAssetStateById(const std::string& assetId) +{ + if (assetId.size() > 0) + { + std::string sigsKey = assetId; + std::string instanceId = ""; + if (Singleton::exists()) + { + I_InstanceAwareness* instance = Singleton::Consume::by(); + Maybe uniqueId = instance->getUniqueID(); + if (uniqueId.ok()) + { + instanceId = uniqueId.unpack(); + sigsKey += "/" + instanceId; + } + } + std::unordered_map>::iterator it; + it = m_AssetBasedWaapSigs.find(sigsKey); + + if (it != m_AssetBasedWaapSigs.end()) + { + return it->second; + } + + if (m_basicWaapSigs == NULL) { + dbgWarning(D_WAAP) << + "WaapAssetStatesManager::Impl::getWaapAssetStateById(): ERROR: m_basicWaapSigs == NULL!"; + return std::shared_ptr(nullptr); + } + + std::shared_ptr newWaapSigs = CreateWaapSigsForAsset(m_basicWaapSigs, assetId, instanceId); + + if (newWaapSigs) + { + m_AssetBasedWaapSigs[sigsKey] = newWaapSigs; + } + + return newWaapSigs; + } + + return std::shared_ptr(nullptr); +} + +void WaapAssetStatesManager::Impl::setAssetDirectoryPath(const std::string &assetDirectoryPath) +{ + m_assetDirectoryPath = assetDirectoryPath; +} + +std::shared_ptr +WaapAssetStatesManager::Impl::CreateWaapSigsForAsset(const std::shared_ptr& pWaapAssetState, + const std::string& assetId, + const std::string& instanceId) +{ + std::string assetPath = + getConfigurationWithDefault(m_assetDirectoryPath, "waap data", "base folder") + + assetId; + if (instanceId != "") + { + assetPath += "/" + instanceId; + } + if (!NGEN::Filesystem::exists(assetPath)) + { + if (!NGEN::Filesystem::makeDirRecursive(assetPath)) + { + dbgWarning(D_WAAP) + << "WaapAssetStatesManager::CreateWaapSigsForAsset() can't create asset folder. " + << "Directory: " + << assetPath; + return std::shared_ptr(nullptr); + } + } + + dbgTrace(D_WAAP) << "WaapAssetStatesManager::CreateWaapSigsForAsset() assetPath is: " << assetPath; + + if (pWaapAssetState == NULL) { + dbgWarning(D_WAAP) << + "WaapAssetStatesManager::CreateWaapSigsForAsset(): failed to create a WaapAssetState object"; + return std::shared_ptr(nullptr); + + } + + std::string basePath = pWaapAssetState->getSignaturesScoresFilePath(); + size_t lastSlash = basePath.find_last_of('/'); + std::string assetScoresPath = assetPath + + ((lastSlash == std::string::npos) ? basePath : basePath.substr(lastSlash)); + dbgTrace(D_WAAP) << "WaapAssetStatesManager::CreateWaapSigsForAsset() assetScoresPath is: " << assetScoresPath; + return std::make_shared(pWaapAssetState, assetScoresPath, assetId); +} diff --git a/components/security_apps/waap/waap_clib/WaapAssetStatesManager.h b/components/security_apps/waap/waap_clib/WaapAssetStatesManager.h new file mode 100755 index 0000000..b900018 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapAssetStatesManager.h @@ -0,0 +1,71 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "singleton.h" +#include "Signatures.h" +#include +#include +#include + +//forward decleration. +class WaapAssetState; + +class I_WaapAssetStatesManager { +public: + virtual bool initBasicWaapSigs(const std::string& sigsFname, const std::string& sigScoresFname) = 0; + virtual std::shared_ptr getWaapAssetStateGlobal() = 0; + virtual std::shared_ptr getWaapAssetStateById(const std::string& assetId) = 0; + virtual void setAssetDirectoryPath(const std::string &assetDirectoryPath) = 0; +}; + +class WaapAssetStatesManager : Singleton::Provide { +public: + WaapAssetStatesManager(); + virtual ~WaapAssetStatesManager(); + + void preload(); + virtual bool initBasicWaapSigs(const std::string& sigsFname, const std::string& sigScoresFname); + virtual std::shared_ptr getWaapAssetStateGlobal(); + virtual std::shared_ptr getWaapAssetStateById(const std::string& assetId); + + virtual void setAssetDirectoryPath(const std::string &assetDirectoryPath); + + class Impl; +protected: + std::unique_ptr pimpl; +}; + +class WaapAssetStatesManager::Impl : Singleton::Provide::From +{ +public: + Impl(); + virtual ~Impl(); + + virtual bool initBasicWaapSigs(const std::string& sigsFname, const std::string& sigScoresFname); + virtual std::shared_ptr getWaapAssetStateGlobal(); + virtual std::shared_ptr getWaapAssetStateById(const std::string& assetId); + virtual void setAssetDirectoryPath(const std::string &assetDirectoryPath); + +private: + std::shared_ptr + CreateWaapSigsForAsset(const std::shared_ptr& pWaapAssetState, + const std::string& assetId, + const std::string& instanceId); + + std::shared_ptr m_signatures; + std::shared_ptr m_basicWaapSigs; + std::unordered_map> m_AssetBasedWaapSigs; + std::string m_assetDirectoryPath; +}; diff --git a/components/security_apps/waap/waap_clib/WaapConfigApi.cc b/components/security_apps/waap/waap_clib/WaapConfigApi.cc new file mode 100755 index 0000000..c9a1c89 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapConfigApi.cc @@ -0,0 +1,114 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapConfigApi.h" +#include "Waf2Util.h" + +#include "telemetry.h" + +using namespace std; + +USE_DEBUG_FLAG(D_WAAP); + +const string WaapConfigAPI::s_PracticeSubType = "Web API"; +set WaapConfigAPI::assets_ids{}; +set WaapConfigAPI::assets_ids_aggregation{}; + +bool +WaapConfigAPI::getWaapAPIConfig(WaapConfigAPI& ngenAPIConfig) { + auto &maybe_ngen_config = getConfiguration( + "WAAP", + "WebAPISecurity" + ); + + if (!maybe_ngen_config.ok()) { + dbgDebug(D_WAAP) << "Unable to get WAAP WebAPISecurity from configuration" << maybe_ngen_config.getErr(); + return false; + } + + ngenAPIConfig = maybe_ngen_config.unpack(); + return true; +} + +WaapConfigAPI::WaapConfigAPI() : WaapConfigBase() +{} + +void +WaapConfigAPI::notifyAssetsCount() +{ + WaapConfigAPI::assets_ids = WaapConfigAPI::assets_ids_aggregation; + AssetCountEvent(AssetType::API, WaapConfigAPI::assets_ids.size()).notify(); +} + +void +WaapConfigAPI::clearAssetsCount() +{ + WaapConfigAPI::assets_ids_aggregation.clear(); +} + +#if 0 // maybe will be used in the future +WaapConfigAPI::WaapConfigAPI( + bool autonomousSecurity, + string autonomousSecurityLevel, + string assetId, + string assetName, + string practiceId, + string practiceName, + string ruleId, + string ruleName, + bool schemaValidation) : + WaapConfigBase( + autonomousSecurity, + autonomousSecurityLevel, + assetId, + assetName, + practiceId, + practiceName, + ruleId, + ruleName), + m_schemaValidation(schemaValidation) +{ +} +#endif + +void WaapConfigAPI::load(cereal::JSONInputArchive& ar) +{ + // order has affect - we need to call base last because of triggers and overrides + readJSONByCereal(ar); + + WaapConfigBase::load(ar); + assets_ids_aggregation.insert(m_assetId); +} + +void WaapConfigAPI::readJSONByCereal(cereal::JSONInputArchive &ar) +{ +} + +bool WaapConfigAPI::operator==(const WaapConfigAPI& other) const +{ + const WaapConfigBase* configBase = this; + const WaapConfigBase& configBaseOther = other; + + return *configBase == configBaseOther; +} + +void WaapConfigAPI::printMe(ostream& os) const +{ + WaapConfigBase::printMe(os); +} + +const string& WaapConfigAPI::get_PracticeSubType() const +{ + return s_PracticeSubType; +} + diff --git a/components/security_apps/waap/waap_clib/WaapConfigApi.h b/components/security_apps/waap/waap_clib/WaapConfigApi.h new file mode 100755 index 0000000..ef3b4d8 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapConfigApi.h @@ -0,0 +1,60 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#ifndef __WAAP_CONFIG_API_H__ +#define __WAAP_CONFIG_API_H__ + +#include + +#include "WaapConfigBase.h" +#include "log_generator.h" +#include "debug.h" + +class WaapConfigAPI : public WaapConfigBase +{ +public: + WaapConfigAPI(); +#if 0 // maybe will be used in the future + WaapConfigAPI( + bool autonomousSecurity, + std::string autonomousSecurityLevel, + std::string assetId, + std::string assetName, + std::string practiceId, + std::string practiceName, + std::string ruleId, + std::string ruleName, + bool schemaValidation); +#endif + + void load(cereal::JSONInputArchive& ar); + bool operator==(const WaapConfigAPI& other) const; + void printMe(std::ostream& os) const; + + virtual const std::string& get_PracticeSubType() const; + static bool getWaapAPIConfig(WaapConfigAPI &ngenAPIConfig); + static void notifyAssetsCount(); + static void clearAssetsCount(); + +private: + void readJSONByCereal(cereal::JSONInputArchive&ar); + + std::string m_schemaValidationPoicyStatusMessage; + + static const std::string s_PracticeSubType; + static std::set assets_ids; + static std::set assets_ids_aggregation; +}; + +#endif // __WAAP_CONFIG_API_H__ diff --git a/components/security_apps/waap/waap_clib/WaapConfigApplication.cc b/components/security_apps/waap/waap_clib/WaapConfigApplication.cc new file mode 100755 index 0000000..3f499b4 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapConfigApplication.cc @@ -0,0 +1,87 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapConfigApplication.h" +#include "telemetry.h" + +using namespace std; + +USE_DEBUG_FLAG(D_WAAP); + +const string WaapConfigApplication::s_PracticeSubType = "Web Application"; +set WaapConfigApplication::assets_ids{}; +set WaapConfigApplication::assets_ids_aggregation{}; + +bool WaapConfigApplication::getWaapSiteConfig(WaapConfigApplication& ngenSiteConfig) { + auto &maybe_ngen_config = getConfiguration( + "WAAP", + "WebApplicationSecurity" + ); + + if (!maybe_ngen_config.ok()) + { + dbgDebug(D_WAAP) << maybe_ngen_config.getErr(); + return false; + } + + ngenSiteConfig = maybe_ngen_config.unpack(); + return true; +} + +WaapConfigApplication::WaapConfigApplication() : + WaapConfigBase() +{ +} + +void +WaapConfigApplication::notifyAssetsCount() +{ + WaapConfigApplication::assets_ids = WaapConfigApplication::assets_ids_aggregation; + AssetCountEvent(AssetType::WEB, WaapConfigApplication::assets_ids.size()).notify(); +} + +void +WaapConfigApplication::clearAssetsCount() +{ + WaapConfigApplication::assets_ids_aggregation.clear(); +} + +const string& WaapConfigApplication::get_PracticeSubType() const +{ + return s_PracticeSubType; +} + +void WaapConfigApplication::load(cereal::JSONInputArchive& ar) +{ + WaapConfigBase::load(ar); + loadOpenRedirectPolicy(ar); + loadErrorDisclosurePolicy(ar); + loadCsrfPolicy(ar); + loadSecurityHeadersPolicy(ar); + + assets_ids_aggregation.insert(m_assetId); +} + + +bool WaapConfigApplication::operator==(const WaapConfigApplication& other) const +{ + const WaapConfigBase* configBase = this; + const WaapConfigBase& configBaseOther = other; + + return *configBase==configBaseOther; +} + +void WaapConfigApplication::printMe(ostream& os) const +{ + WaapConfigBase::printMe(os); +} diff --git a/components/security_apps/waap/waap_clib/WaapConfigApplication.h b/components/security_apps/waap/waap_clib/WaapConfigApplication.h new file mode 100755 index 0000000..f6149d1 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapConfigApplication.h @@ -0,0 +1,57 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#ifndef __WAAP_CONFIG_APPLICATION_H__ +#define __WAAP_CONFIG_APPLICATION_H__ + +#include + +#include "WaapConfigBase.h" +#include "log_generator.h" +#include "debug.h" + +class WaapConfigApplication : public WaapConfigBase +{ +public: + WaapConfigApplication(); +#if 0 // maybe will be used in the future + WaapConfigApplication( + bool autonomousSecurity, + std::string autonomousSecurityLevel, + std::string assetId, + std::string assetName, + std::string practiceId, + std::string practiceName, + std::string ruleId, + std::string ruleName, + bool botProtection); +#endif + + bool operator==(const WaapConfigApplication& other) const; + + virtual const std::string& get_PracticeSubType() const; + + void load(cereal::JSONInputArchive& ar); + void printMe(std::ostream& os) const; + static bool getWaapSiteConfig(WaapConfigApplication& ngenSiteConfig); + static void notifyAssetsCount(); + static void clearAssetsCount(); + +private: + static const std::string s_PracticeSubType; + static std::set assets_ids; + static std::set assets_ids_aggregation; +}; + +#endif // __WAAP_CONFIG_APPLICATION_H__ diff --git a/components/security_apps/waap/waap_clib/WaapConfigBase.cc b/components/security_apps/waap/waap_clib/WaapConfigBase.cc new file mode 100755 index 0000000..63d454f --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapConfigBase.cc @@ -0,0 +1,450 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapConfigBase.h" +#include +#include "WaapConfigApplication.h" +#include "WaapOverride.h" +#include "WaapTrigger.h" +#include "WaapOpenRedirectPolicy.h" +#include "CsrfPolicy.h" +#include "WaapErrorDisclosurePolicy.h" +#include "TrustedSources.h" +#include "Waf2Util.h" + +USE_DEBUG_FLAG(D_WAAP_ULIMITS); +using boost::algorithm::to_lower_copy; + +WaapConfigBase::WaapConfigBase() + : + m_assetId(""), + m_autonomousSecurityLevel(""), + m_autonomousSecurity(false), + m_assetName(""), + m_practiceId(""), + m_practiceName(""), + m_ruleId(""), + m_ruleName(""), + m_overridePolicy(nullptr), + m_triggerPolicy(nullptr), + m_trustedSourcesPolicy(nullptr), + m_waapParameters(nullptr), + m_openRedirectPolicy(nullptr), + m_errorDisclosurePolicy(nullptr), + m_csrfPolicy(nullptr), + m_rateLimitingPolicy(nullptr), + m_errorLimitingPolicy(nullptr), + m_errorLimiting(nullptr), + m_userLimitsPolicy(nullptr), + m_securityHeadersPolicy(nullptr) +{ + m_blockingLevel = BlockingLevel::NO_BLOCKING; +} + +void WaapConfigBase::load(cereal::JSONInputArchive& ar) +{ + readJSONByCereal(ar); + loadTriggersPolicy(ar); + loadOverridePolicy(ar); + loadTrustedSourcesPolicy(ar); + loadWaapParametersPolicy(ar); + loadUserLimitsPolicy(ar); + loadRateLimitingPolicy(ar); + loadErrorLimitingPolicy(ar); +} + +void WaapConfigBase::readJSONByCereal(cereal::JSONInputArchive& ar) +{ + ar( + cereal::make_nvp("webAttackMitigation", m_autonomousSecurity), + cereal::make_nvp("webAttackMitigationAction", m_autonomousSecurityLevel), + cereal::make_nvp("practiceId", m_practiceId), + cereal::make_nvp("practiceName", m_practiceName), + cereal::make_nvp("assetId", m_assetId), + cereal::make_nvp("assetName", m_assetName), + cereal::make_nvp("ruleId", m_ruleId), + cereal::make_nvp("ruleName", m_ruleName) + ); + + m_blockingLevel = blockingLevelBySensitivityStr(m_autonomousSecurityLevel); +} + +void WaapConfigBase::loadCsrfPolicy(cereal::JSONInputArchive& ar) +{ + std::string failMessage = "Failed to load the CSRF policy of the current rule: " + + m_ruleName + ": "; + + try { + m_csrfPolicy = std::make_shared(ar); + } + catch (std::runtime_error& e) { + ar.setNextName(nullptr); + dbgWarning(D_WAAP) << failMessage << e.what(); + m_csrfPolicy = std::make_shared(); + } +} + +void WaapConfigBase::loadSecurityHeadersPolicy(cereal::JSONInputArchive& ar) +{ + std::string failMessage = "Failed to load the Security Headers policy of the current rule: " + + m_ruleName + ": "; + + try { + m_securityHeadersPolicy = std::make_shared(ar); + } + catch (std::runtime_error& e) { + ar.setNextName(nullptr); + // Feature is currently not supported by the UI, thus changing debug level to debug. + dbgDebug(D_WAAP) << failMessage << e.what(); + m_securityHeadersPolicy = nullptr; + } +} + +void WaapConfigBase::loadOverridePolicy(cereal::JSONInputArchive& ar) +{ + std::string failMessage = "Failed to load the WAAP Overrides of the current rule: " + + m_ruleName + ": "; + + try { + m_overridePolicy = std::make_shared(ar); + } + catch (std::runtime_error& e) { + ar.setNextName(nullptr); + dbgWarning(D_WAAP) << failMessage << e.what(); + m_overridePolicy = nullptr; + } +} + +void WaapConfigBase::loadTriggersPolicy(cereal::JSONInputArchive& ar) +{ + std::string failMessage = "Failed to load the WAAP Triggers of the current rule: " + + m_ruleName + ": "; + try { + m_triggerPolicy = std::make_shared(ar); + } + catch (std::runtime_error& e) { + ar.setNextName(nullptr); + dbgWarning(D_WAAP) << failMessage << e.what(); + m_triggerPolicy = nullptr; + } +} + +void WaapConfigBase::loadTrustedSourcesPolicy(cereal::JSONInputArchive& ar) +{ + std::string failMessage = "Failed to load the WAAP Trusted sources of the current rule: " + + m_ruleName + ": "; + try { + m_trustedSourcesPolicy = std::make_shared(ar); + } + catch (std::runtime_error & e) { + ar.setNextName(nullptr); + dbgWarning(D_WAAP) << failMessage << e.what(); + m_trustedSourcesPolicy = nullptr; + } +} + +void WaapConfigBase::loadWaapParametersPolicy(cereal::JSONInputArchive& ar) +{ + std::string failMessage = "Failed to load the WAAP Parameters of the current rule: " + + m_ruleName + ": "; + try { + m_waapParameters = std::make_shared(ar); + } + catch (std::runtime_error & e) { + ar.setNextName(nullptr); + dbgWarning(D_WAAP) << failMessage << e.what(); + m_waapParameters = nullptr; + } +} + +void WaapConfigBase::loadRateLimitingPolicy(cereal::JSONInputArchive& ar) +{ + std::string failMessage = "Failed to load the WAAP Rate Limiting of the current rule: " + + m_ruleName + ": "; + try { + m_rateLimitingPolicy = std::make_shared(ar); + } + catch (std::runtime_error & e) { + ar.setNextName(nullptr); + // Feature is currently not supported by the UI, thus changing debug level to debug. + dbgDebug(D_WAAP) << failMessage << e.what(); + m_rateLimitingPolicy = nullptr; + } +} + +void WaapConfigBase::loadErrorLimitingPolicy(cereal::JSONInputArchive& ar) +{ + std::string failMessage = "Failed to load the WAAP Error Limiting of the current rule: " + + m_ruleName + ": "; + + try { + + m_errorLimiting = std::make_shared(ar); + std::shared_ptr policy; + policy = std::make_shared(); + policy->rules.push_back(Waap::RateLimiting::Policy::Rule()); + policy->rules[0].rate.interval = m_errorLimiting->m_errorLimiterPolicy.interval; + policy->rules[0].rate.events = m_errorLimiting->m_errorLimiterPolicy.events; + policy->rules[0].uriFilter.groupBy = Waap::RateLimiting::Policy::Rule::UriFilter::GroupBy::GLOBAL; + policy->rules[0].sourceFilter.groupBy = Waap::RateLimiting::Policy::Rule::SourceFilter::GroupBy::GLOBAL; + policy->rules[0].uriFilter.scope = Waap::RateLimiting::Policy::Rule::UriFilter::Scope::ALL; + policy->rules[0].sourceFilter.scope = Waap::RateLimiting::Policy::Rule::SourceFilter::Scope::ALL; + policy->m_rateLimiting.enable = m_errorLimiting->getErrorLimitingEnforcementStatus(); + + if (m_errorLimiting->m_errorLimiterPolicy.type == "quarantine") { + policy->rules[0].action.type = Waap::RateLimiting::Policy::Rule::Action::Type::QUARANTINE; + policy->rules[0].action.quarantineTimeSeconds = m_errorLimiting->m_errorLimiterPolicy.blockingTime; + } + else if (m_errorLimiting->m_errorLimiterPolicy.type == "rate limit") { + policy->rules[0].action.type = Waap::RateLimiting::Policy::Rule::Action::Type::RATE_LIMIT; + } + else if (m_errorLimiting->m_errorLimiterPolicy.type == "detect") { + policy->rules[0].action.type = Waap::RateLimiting::Policy::Rule::Action::Type::DETECT; + } + + m_errorLimitingPolicy = policy; + } + catch (std::runtime_error & e) { + ar.setNextName(nullptr); + // Feature is currently not supported by the UI, thus changing debug level to debug. + dbgDebug(D_WAAP) << failMessage << e.what(); + m_errorLimiting = nullptr; + m_errorLimitingPolicy = nullptr; + } + +} + +void WaapConfigBase::loadOpenRedirectPolicy(cereal::JSONInputArchive& ar) +{ + std::string failMessage = "Failed to load the WAAP OpenRedirect policy"; + try { + m_openRedirectPolicy = std::make_shared(ar); + } + catch (std::runtime_error & e) { + ar.setNextName(nullptr); + dbgWarning(D_WAAP) << failMessage << e.what(); + // TODO:: change the default back to nullptr when implemeted in hook + // m_openRedirectPolicy = nullptr; + // Now (until hook is implemented) the default is enabled+enforced + m_openRedirectPolicy = std::make_shared(); + } +} + +void WaapConfigBase::loadErrorDisclosurePolicy(cereal::JSONInputArchive& ar) +{ + std::string failMessage = "Failed to load the WAAP Information Disclosure policy"; + try { + m_errorDisclosurePolicy = std::make_shared(ar); + } + catch (std::runtime_error & e) { + ar.setNextName(nullptr); + dbgWarning(D_WAAP) << failMessage << e.what(); + m_errorDisclosurePolicy = nullptr; + } +} + +void WaapConfigBase::loadUserLimitsPolicy(cereal::JSONInputArchive& ar) +{ + try { + m_userLimitsPolicy = std::make_shared(ar); + dbgInfo(D_WAAP_ULIMITS) << "[USER LIMITS] policy loaded:\n" << *m_userLimitsPolicy; + } + catch (std::runtime_error & e) { + ar.setNextName(nullptr); + m_userLimitsPolicy = std::make_shared(); + dbgInfo(D_WAAP_ULIMITS) << "[USER LIMITS] default policy loaded:\n" << *m_userLimitsPolicy; + } +} + +bool WaapConfigBase::operator==(const WaapConfigBase& other) const +{ + return + m_autonomousSecurity == other.m_autonomousSecurity && + m_autonomousSecurityLevel == other.m_autonomousSecurityLevel && + m_practiceId == other.m_practiceId && + m_practiceName == other.m_practiceName && + m_ruleId == other.m_ruleId && + m_ruleName == other.m_ruleName && + m_assetId == other.m_assetId && + m_assetName == other.m_assetName && + Waap::Util::compareObjects(m_triggerPolicy, other.m_triggerPolicy) && + Waap::Util::compareObjects(m_overridePolicy, other.m_overridePolicy) && + Waap::Util::compareObjects(m_trustedSourcesPolicy, other.m_trustedSourcesPolicy) && + Waap::Util::compareObjects(m_waapParameters, other.m_waapParameters) && + Waap::Util::compareObjects(m_openRedirectPolicy, other.m_openRedirectPolicy) && + Waap::Util::compareObjects(m_errorDisclosurePolicy, other.m_errorDisclosurePolicy) && + Waap::Util::compareObjects(m_rateLimitingPolicy, other.m_rateLimitingPolicy) && + Waap::Util::compareObjects(m_errorLimitingPolicy, other.m_errorLimitingPolicy) && + Waap::Util::compareObjects(m_csrfPolicy, other.m_csrfPolicy) && + Waap::Util::compareObjects(m_userLimitsPolicy, other.m_userLimitsPolicy) && + Waap::Util::compareObjects(m_securityHeadersPolicy, other.m_securityHeadersPolicy); +} + +void WaapConfigBase::printMe(std::ostream& os) const +{ + os << m_autonomousSecurity << ", " << m_autonomousSecurityLevel; + os << ", " << m_ruleId << ", " << m_ruleName; + os << ", " << m_practiceId << ", " << m_practiceName << ", " << m_assetId << ", " << m_assetName; +} + +const std::string& WaapConfigBase::get_AssetId() const +{ + return m_assetId; +} + +const std::string& WaapConfigBase::get_AssetName() const +{ + return m_assetName; +} + +const std::string& WaapConfigBase::get_PracticeId() const +{ + return m_practiceId; +} + +const std::string& WaapConfigBase::get_PracticeName() const +{ + return m_practiceName; +} + +const std::string& WaapConfigBase::get_RuleId() const +{ + return m_ruleId; +} + +const std::string& WaapConfigBase::get_RuleName() const +{ + return m_ruleName; +} + +const bool& WaapConfigBase::get_WebAttackMitigation() const +{ + return m_autonomousSecurity; +} + +const std::string& WaapConfigBase::get_WebAttackMitigationAction() const +{ + return m_autonomousSecurityLevel; +} + +AttackMitigationMode +WaapConfigBase::get_WebAttackMitigationMode(const IWaapConfig& siteConfig) +{ + AttackMitigationMode attackMitigationMode = AttackMitigationMode::UNKNOWN; + if (siteConfig.get_WebAttackMitigation()) { + attackMitigationMode = (siteConfig.get_BlockingLevel() == BlockingLevel::NO_BLOCKING) ? + AttackMitigationMode::LEARNING : AttackMitigationMode::PREVENT; + } + else { + attackMitigationMode = AttackMitigationMode::DISABLED; + } + return attackMitigationMode; +} + +const char* +WaapConfigBase::get_WebAttackMitigationModeStr(const IWaapConfig& siteConfig) +{ + switch(get_WebAttackMitigationMode(siteConfig)) { + case AttackMitigationMode::DISABLED: + return "DISABLED"; + case AttackMitigationMode::LEARNING: + return "LEARNING"; + case AttackMitigationMode::PREVENT: + return "PREVENT"; + default: + return "UNKNOWN"; + } +} + +const BlockingLevel& WaapConfigBase::get_BlockingLevel() const +{ + return m_blockingLevel; +} + +const std::shared_ptr& WaapConfigBase::get_OverridePolicy() const +{ + return m_overridePolicy; +} + +const std::shared_ptr& WaapConfigBase::get_TriggerPolicy() const +{ + return m_triggerPolicy; +} + +const std::shared_ptr& WaapConfigBase::get_TrustedSourcesPolicy() const +{ + return m_trustedSourcesPolicy; +} + +const std::shared_ptr& WaapConfigBase::get_CsrfPolicy() const +{ + return m_csrfPolicy; +} + +const std::shared_ptr& WaapConfigBase::get_WaapParametersPolicy() const +{ + return m_waapParameters; +} + +const std::shared_ptr& WaapConfigBase::get_RateLimitingPolicy() const +{ + return m_rateLimitingPolicy; +} + +const std::shared_ptr& WaapConfigBase::get_ErrorLimitingPolicy() const +{ + return m_errorLimitingPolicy; +} + +const std::shared_ptr& WaapConfigBase::get_OpenRedirectPolicy() const +{ + return m_openRedirectPolicy; +} + +const std::shared_ptr& WaapConfigBase::get_ErrorDisclosurePolicy() const +{ + return m_errorDisclosurePolicy; +} + +const std::shared_ptr& WaapConfigBase::get_SecurityHeadersPolicy() const +{ + return m_securityHeadersPolicy; +} + +const std::shared_ptr& WaapConfigBase::get_UserLimitsPolicy() const +{ + return m_userLimitsPolicy; +} + +BlockingLevel WaapConfigBase::blockingLevelBySensitivityStr(const std::string& sensitivity) const +{ + std::string sensitivityLower = to_lower_copy(sensitivity); + + if (sensitivityLower == "transparent") + { + return BlockingLevel::NO_BLOCKING; + } + else if (sensitivityLower == "low") + { + return BlockingLevel::LOW_BLOCKING_LEVEL; + } + else if (sensitivityLower == "balanced") + { + return BlockingLevel::MEDIUM_BLOCKING_LEVEL; + } + else if (sensitivityLower == "high") + { + return BlockingLevel::HIGH_BLOCKING_LEVEL; + } + return BlockingLevel::NO_BLOCKING; +} diff --git a/components/security_apps/waap/waap_clib/WaapConfigBase.h b/components/security_apps/waap/waap_clib/WaapConfigBase.h new file mode 100755 index 0000000..cc15ebf --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapConfigBase.h @@ -0,0 +1,107 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#ifndef __WAAP_CONFIG_BASE_H__ +#define __WAAP_CONFIG_BASE_H__ + +#include "i_waapConfig.h" +#include "WaapOverride.h" +#include "WaapTrigger.h" +#include "WaapOpenRedirectPolicy.h" +#include "WaapErrorDisclosurePolicy.h" +#include "ErrorLimiting.h" +#include "CsrfPolicy.h" +#include "SecurityHeadersPolicy.h" +#include "UserLimitsPolicy.h" +#include "TrustedSources.h" +#include "Waf2Util.h" +#include "debug.h" + +class WaapConfigBase : public IWaapConfig +{ +public: + static AttackMitigationMode get_WebAttackMitigationMode(const IWaapConfig& siteConfig); + static const char* get_WebAttackMitigationModeStr(const IWaapConfig& siteConfig); + + bool operator==(const WaapConfigBase& other) const; + + virtual const std::string& get_AssetId() const; + virtual const std::string& get_AssetName() const; + virtual const BlockingLevel& get_BlockingLevel() const; + virtual const std::string& get_PracticeId() const; + virtual const std::string& get_PracticeName() const; + virtual const std::string& get_RuleId() const; + virtual const std::string& get_RuleName() const; + virtual const bool& get_WebAttackMitigation() const; + virtual const std::string& get_WebAttackMitigationAction() const; + + virtual const std::shared_ptr& get_OverridePolicy() const; + virtual const std::shared_ptr& get_TriggerPolicy() const; + virtual const std::shared_ptr& get_TrustedSourcesPolicy() const; + virtual const std::shared_ptr& get_WaapParametersPolicy() const; + virtual const std::shared_ptr& get_OpenRedirectPolicy() const; + virtual const std::shared_ptr& get_ErrorDisclosurePolicy() const; + virtual const std::shared_ptr& get_CsrfPolicy() const; + virtual const std::shared_ptr& get_RateLimitingPolicy() const; + virtual const std::shared_ptr& get_SecurityHeadersPolicy() const; + virtual const std::shared_ptr& get_ErrorLimitingPolicy() const; + virtual const std::shared_ptr& get_UserLimitsPolicy() const; + + virtual void printMe(std::ostream& os) const; + +protected: + WaapConfigBase(); + void load(cereal::JSONInputArchive& ar); + void loadOpenRedirectPolicy(cereal::JSONInputArchive& ar); + void loadErrorDisclosurePolicy(cereal::JSONInputArchive& ar); + void loadCsrfPolicy(cereal::JSONInputArchive& ar); + void loadRateLimitingPolicy(cereal::JSONInputArchive& ar); + void loadSecurityHeadersPolicy(cereal::JSONInputArchive& ar); + void loadErrorLimitingPolicy(cereal::JSONInputArchive& ar); + + std::string m_assetId; +private: + void loadOverridePolicy(cereal::JSONInputArchive& ar); + void loadTriggersPolicy(cereal::JSONInputArchive& ar); + void loadTrustedSourcesPolicy(cereal::JSONInputArchive& ar); + void loadWaapParametersPolicy(cereal::JSONInputArchive& ar); + void loadUserLimitsPolicy(cereal::JSONInputArchive& ar); + + void readJSONByCereal(cereal::JSONInputArchive& ar); + BlockingLevel blockingLevelBySensitivityStr(const std::string& sensitivity) const; + + std::string m_autonomousSecurityLevel; + bool m_autonomousSecurity; + std::string m_assetName; + BlockingLevel m_blockingLevel; + std::string m_practiceId; + std::string m_practiceName; + std::string m_ruleId; + std::string m_ruleName; + + std::shared_ptr m_overridePolicy; + std::shared_ptr m_triggerPolicy; + std::shared_ptr m_trustedSourcesPolicy; + std::shared_ptr m_waapParameters; + std::shared_ptr m_openRedirectPolicy; + std::shared_ptr m_errorDisclosurePolicy; + std::shared_ptr m_csrfPolicy; + std::shared_ptr m_rateLimitingPolicy; + std::shared_ptr m_errorLimitingPolicy; + std::shared_ptr m_errorLimiting; + std::shared_ptr m_userLimitsPolicy; + std::shared_ptr m_securityHeadersPolicy; +}; + +#endif // __WAAP_CONFIG_BASE_H__ diff --git a/components/security_apps/waap/waap_clib/WaapConversions.cc b/components/security_apps/waap/waap_clib/WaapConversions.cc new file mode 100644 index 0000000..cd5fd01 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapConversions.cc @@ -0,0 +1,73 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapConversions.h" +#include "debug.h" + +USE_DEBUG_FLAG(D_WAAP); + +namespace Waap { +namespace Conversions { + ThreatLevel convertFinalScoreToThreatLevel(double finalScore) + { + if (finalScore == NO_THREAT_FINAL_SCORE) + { + return NO_THREAT; + } + if (finalScore < INFO_THREAT_THRESHOLD) + { + return THREAT_INFO; + } + if (finalScore < LOW_THREAT_THRESHOLD) + { + return LOW_THREAT; + } + if (finalScore < MED_THREAT_THRESHOLD) + { + return MEDIUM_THREAT; + } + return HIGH_THREAT; + } + + bool shouldDoWafBlocking(const IWaapConfig* pWaapConfig, ThreatLevel threatLevel) + { + if (pWaapConfig == NULL) + { + return false; + } + + if (threatLevel <= THREAT_INFO) + { + return false; + } + + BlockingLevel blockLevel = pWaapConfig->get_BlockingLevel(); + + switch (blockLevel) + { + case BlockingLevel::LOW_BLOCKING_LEVEL: + return threatLevel >= HIGH_THREAT; + case BlockingLevel::MEDIUM_BLOCKING_LEVEL: + return threatLevel >= MEDIUM_THREAT; + case BlockingLevel::HIGH_BLOCKING_LEVEL: + return true; + case BlockingLevel::NO_BLOCKING: + return false; + default: + dbgDebug(D_WAAP) << "Invalid blocking level in WAAP Config: " << static_cast::type>(blockLevel); + } + return false; + } +} +} diff --git a/components/security_apps/waap/waap_clib/WaapConversions.h b/components/security_apps/waap/waap_clib/WaapConversions.h new file mode 100644 index 0000000..ec99dda --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapConversions.h @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __WAAP_CONVERSIONS_H__ +#define __WAAP_CONVERSIONS_H__ + +#include "WaapEnums.h" +#include "i_waapConfig.h" + +namespace Waap { +namespace Conversions { + ThreatLevel convertFinalScoreToThreatLevel(double finalScore); + bool shouldDoWafBlocking(const IWaapConfig* pSitePolicy, ThreatLevel threatLevel); +} +} + +#endif // __WAAP_CONVERSIONS_H__ diff --git a/components/security_apps/waap/waap_clib/WaapDecision.cc b/components/security_apps/waap/waap_clib/WaapDecision.cc new file mode 100755 index 0000000..a03e4eb --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapDecision.cc @@ -0,0 +1,208 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapDecision.h" +#include "OpenRedirectDecision.h" +#include "debug.h" +#include +#include + +USE_DEBUG_FLAG(D_WAAP); + +WaapDecision::WaapDecision() : + m_json(""), + m_decisionFactory() +{ +} + +std::shared_ptr +WaapDecision::getDecision(DecisionType type) const +{ + return m_decisionFactory.getDecision(type); +} + +void +WaapDecision::orderDecisions() +{ + const DecisionsArr& decisions = m_decisionFactory.getDecisions(); + dbgTrace(D_WAAP) << "Original: " << decisions; + std::copy_if(decisions.begin(), + decisions.end(), + std::back_inserter(m_ordered_decisions), + [](const std::shared_ptr& decision) { + return decision && (decision->shouldBlock() || decision->shouldLog()); + }); + if (!m_ordered_decisions.empty()) { + dbgTrace(D_WAAP) << "Reduced: " << m_ordered_decisions; + m_ordered_decisions.sort(sortDecisions); + dbgTrace(D_WAAP) << "Sorted: " << m_ordered_decisions; + } + + setIteratorToFirstDecisionToLog(); +} + +void WaapDecision::setIteratorToFirstDecisionToLog() +{ + m_first_decision_to_log = + std::find_if( + m_ordered_decisions.begin(), + m_ordered_decisions.end(), + [](const std::shared_ptr& decision) + { + return decision && decision->shouldLog(); + }); +} + +bool +WaapDecision::sortDecisions(const std::shared_ptr& lhs, const std::shared_ptr& rhs) +{ + if (lhs->shouldBlock() && !rhs->shouldBlock()) { + return true; + } + else if (!lhs->shouldBlock() && rhs->shouldBlock()) { + return false; + } + else if (lhs->shouldLog() && !rhs->shouldLog()) { + return true; + } + else if (!lhs->shouldLog() && rhs->shouldLog()) { + return false; + } + else if (lhs->getType() < rhs->getType()) { + return true; + } + return false; +} + +std::ostream& operator<<(std::ostream& os, const DecisionsArr& decisions) +{ + os << "Decision(block, log): "; + for (auto decision : decisions) + { + os << decision->getTypeStr() << "(" << decision->shouldBlock() << ", " << + decision->shouldLog() << ") "; + } + return os; +} + +std::ostream& operator<<(std::ostream& os, const std::list>& decisions) +{ + os << "Decision(block, log): "; + for (auto decision : decisions) + { + os << decision->getTypeStr() << "(" << decision->shouldBlock() << ", " << + decision->shouldLog() << ") "; + } + return os; +} + +bool WaapDecision::getShouldBlockFromHighestPriorityDecision() const +{ + if (!m_ordered_decisions.empty()) + { + return m_ordered_decisions.front()->shouldBlock(); + } + return false; +} + +bool WaapDecision::anyDecisionsToLogOrBlock() const +{ + return !m_ordered_decisions.empty(); +} + +DecisionType WaapDecision::getHighestPriorityDecisionToLog() const +{ + if (m_first_decision_to_log == m_ordered_decisions.end()) + { + return DecisionType::NO_WAAP_DECISION; + } + return (*m_first_decision_to_log)->getType(); +} + +void WaapDecision::getIncidentLogFields( + const std::string& responseStatus, + std::string& incidentDetails, + std::string& incidentType +) const +{ + incidentDetails.clear(); + incidentType.clear(); + + for (decision_list::const_iterator iter = m_first_decision_to_log; iter != m_ordered_decisions.end(); ++iter) + { + const std::shared_ptr& nextDecision = *iter; + std::string tempIncidentDetails; + std::string tempIncidentType; + + if (!nextDecision->shouldLog()) + { + continue; + } + + bool isRelevant = true; + switch (nextDecision->getType()) + { + case OPEN_REDIRECT_DECISION: + { + tempIncidentDetails = "OpenRedirect attack detected (" + + std::dynamic_pointer_cast(nextDecision)->getLink() + ")"; + tempIncidentType = "Cross Site Redirect"; + break; + } + + case ERROR_LIMITING_DECISION: + { + tempIncidentDetails = "Application scanning detected"; + tempIncidentType = "Error Limit"; + break; + } + + case RATE_LIMITING_DECISION: + { + tempIncidentDetails = "High request rate detected"; + tempIncidentType = "Request Rate Limit"; + break; + } + case ERROR_DISCLOSURE_DECISION: + tempIncidentDetails = "Information disclosure in server response detected"; + tempIncidentDetails += ", response status code: " + responseStatus; + tempIncidentType = "Error Disclosure"; + break; + default: + isRelevant = false; + break; + } + if (isRelevant) { + if (!incidentDetails.empty()) + { + incidentDetails += " and "; + } + if (!incidentType.empty()) + { + incidentType += ", "; + } + incidentDetails += tempIncidentDetails; + incidentType += tempIncidentType; + } + } +} + +void WaapDecision::setJson(const std::string& json) +{ + m_json = json; +} + +std::string WaapDecision::getJson() const +{ + return m_json; +} diff --git a/components/security_apps/waap/waap_clib/WaapDecision.h b/components/security_apps/waap/waap_clib/WaapDecision.h new file mode 100755 index 0000000..1b2bcc4 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapDecision.h @@ -0,0 +1,61 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __WAAP_DECISION_H__ +#define __WAAP_DECISION_H__ + +#include +#include +#include +#include +#include "WaapEnums.h" +#include "SingleDecision.h" +#include "DecisionFactory.h" +#include "AutonomousSecurityDecision.h" +#include + +std::ostream& operator<<(std::ostream& os, const std::list>& decisions); +std::ostream& operator<<(std::ostream& os, const DecisionsArr& decisions); +typedef std::list> decision_list; + +class WaapDecision { +public: + WaapDecision(); + std::shared_ptr getDecision(DecisionType type) const; + void orderDecisions(); + static bool + sortDecisions(const std::shared_ptr& lhs, const std::shared_ptr& rhs); + bool getShouldBlockFromHighestPriorityDecision() const; + bool anyDecisionsToLogOrBlock() const; + DecisionType getHighestPriorityDecisionToLog() const; + void getIncidentLogFields( + const std::string& response_status, + std::string& incidentDetails, + std::string& incidentType + ) const; + void setJson(const std::string& json); + std::string getJson() const; + +private: + friend std::ostream& operator<<(std::ostream& os, const DecisionsArr& decisions); + friend std::ostream& operator<<(std::ostream& os, + const std::list>& decisions); + + void setIteratorToFirstDecisionToLog(); + + std::string m_json; + DecisionFactory m_decisionFactory; + decision_list m_ordered_decisions; + decision_list::const_iterator m_first_decision_to_log; +}; +#endif diff --git a/components/security_apps/waap/waap_clib/WaapErrorDisclosurePolicy.cc b/components/security_apps/waap/waap_clib/WaapErrorDisclosurePolicy.cc new file mode 100755 index 0000000..e47c149 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapErrorDisclosurePolicy.cc @@ -0,0 +1,27 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapErrorDisclosurePolicy.h" + +namespace Waap { +namespace ErrorDisclosure { + +bool +Policy::operator==(const Policy &other) const +{ + return enable == other.enable && + enforce == other.enforce; +} + +} +} diff --git a/components/security_apps/waap/waap_clib/WaapErrorDisclosurePolicy.h b/components/security_apps/waap/waap_clib/WaapErrorDisclosurePolicy.h new file mode 100755 index 0000000..2248757 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapErrorDisclosurePolicy.h @@ -0,0 +1,50 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include +#include +#include +#include "debug.h" + +namespace Waap { +namespace ErrorDisclosure { + +struct Policy { + template + Policy(_A &ar) + : + enable(false), + enforce(false) + { + std::string level; + ar(cereal::make_nvp("errorDisclosure", level)); + level = boost::algorithm::to_lower_copy(level); + if (level == "detect") { + enable = true; + } + else if (level == "prevent") { + enable = true; + enforce = true; + } + } + + bool operator==(const Policy &other) const; + + bool enable; + bool enforce; +}; + +} +} diff --git a/components/security_apps/waap/waap_clib/WaapKeywords.cc b/components/security_apps/waap/waap_clib/WaapKeywords.cc new file mode 100644 index 0000000..6a1b8e4 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapKeywords.cc @@ -0,0 +1,47 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapKeywords.h" + +namespace Waap { +namespace Keywords { + +void +computeKeywordsSet(KeywordsSet &keywordsSet, const std::vector &keyword_matches, + const Waap::Util::map_of_stringlists_t &found_patterns) +{ + // Add all detected keyword_matches + keywordsSet.insert(keyword_matches.begin(), keyword_matches.end()); + + for (auto it = found_patterns.begin(); it != found_patterns.end(); ++it) { + const std::string& key = it->first; + const std::vector& keywordsList = it->second; + bool foundPatternNotInMatches = false; + + for (auto pKeyword = keywordsList.begin(); pKeyword != keywordsList.end(); ++pKeyword) { + if (std::find(keyword_matches.begin(), keyword_matches.end(), *pKeyword) != keyword_matches.end()) { + foundPatternNotInMatches = true; + } + } + + // Only add keys from found_patterns for which there are no values in keyword_matches + // The reason is to avoid adding both value and its related key to the same mix, which would + // unjustfully pump up the score for the keywordsSet. + if (!foundPatternNotInMatches) { + keywordsSet.insert(key); + } + } +} + +} +} diff --git a/components/security_apps/waap/waap_clib/WaapKeywords.h b/components/security_apps/waap/waap_clib/WaapKeywords.h new file mode 100644 index 0000000..fdc2ca6 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapKeywords.h @@ -0,0 +1,31 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include "Waf2Util.h" +#include +#include +#include + +namespace Waap { +namespace Keywords { + +typedef std::unordered_set KeywordsSet; +typedef std::vector KeywordsVec; + +void computeKeywordsSet(KeywordsSet &keywordsSet, const std::vector &keyword_matches, + const Waap::Util::map_of_stringlists_t &found_patterns); + +} +} diff --git a/components/security_apps/waap/waap_clib/WaapOpenRedirect.cc b/components/security_apps/waap/waap_clib/WaapOpenRedirect.cc new file mode 100755 index 0000000..2fc4938 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapOpenRedirect.cc @@ -0,0 +1,90 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapOpenRedirect.h" +#include "WaapOpenRedirectPolicy.h" +#include "Waf2Util.h" +#include +#include +#include +#include + +USE_DEBUG_FLAG(D_WAAP); + +// Max number of openredirect URLs extracted from URL parameters, that are stored +#define MAX_OPENREDIRECT_URLS 25 + +namespace Waap { +namespace OpenRedirect { + +void State::collect(const char* v, size_t v_len, const std::string &hostStr) { + std::string urlDomain; + + if (v_len>8 && memcaseinsensitivecmp(v, 8, "https://", 8)) { + // Detect https URL and extract domain name + urlDomain = std::string(v+8, v_len-8); + } + else if (v_len>7 && memcaseinsensitivecmp(v, 7, "http://", 7)) { + // Detect http URL and extract domain name + urlDomain = std::string(v+7, v_len-7); + } + + // urlDomain starts with domain name (without the schema), which can is terminated by the '/' character + urlDomain = urlDomain.substr(0, urlDomain.find('/', 0)); + + // For comparison, consider domain names from the Host: header and from the value URL, without port numbers + std::string urlDomainNoPort = urlDomain.substr(0, urlDomain.find(":", 0)); + std::string hostStrNoPort = hostStr.substr(0, hostStr.find(":", 0)); + + // Avoid adding URLs whose "domain" part is equal to the site's hostname (take from the request's Host header) + // Also, limit number of openredirect URLs we remember + if (!urlDomainNoPort.empty() && urlDomainNoPort != hostStrNoPort && + m_openRedirectUrls.size() < MAX_OPENREDIRECT_URLS) + { + m_openRedirectUrls.insert(boost::algorithm::to_lower_copy(std::string(v, v_len))); + dbgTrace(D_WAAP) << "Waf2Transaction::collectUrlsForOpenRedirect(): adding url '" << + std::string(v, v_len) << "'"; + } +} + +bool +State::testRedirect(const std::string &redirectUrl) const +{ + if (redirectUrl.empty()) { + return false; + } + + std::string redirectUrlLower = boost::algorithm::to_lower_copy(redirectUrl); + + if (!redirectUrlLower.empty()) + { + for (const auto &collectedUrl : m_openRedirectUrls) { + // Detect whether redirect URL (from the Location response header) starts with one of the collected urls + // Note that the collected URLs are already stored lowercase + if (boost::algorithm::starts_with(redirectUrlLower, collectedUrl)) { + return true; + } + } + } + + return false; +} + +bool +State::empty() const +{ + return m_openRedirectUrls.empty(); +} + +} +} diff --git a/components/security_apps/waap/waap_clib/WaapOpenRedirect.h b/components/security_apps/waap/waap_clib/WaapOpenRedirect.h new file mode 100755 index 0000000..3d13619 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapOpenRedirect.h @@ -0,0 +1,34 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include "WaapOpenRedirectPolicy.h" +#include +#include +#include +#include "debug.h" + +namespace Waap { +namespace OpenRedirect { + +class State { +public: + void collect(const char* v, size_t v_len, const std::string &hostStr); + bool testRedirect(const std::string &redirectUrl) const; + bool empty() const; +private: + std::set m_openRedirectUrls; +}; + +} +} diff --git a/components/security_apps/waap/waap_clib/WaapOpenRedirectPolicy.cc b/components/security_apps/waap/waap_clib/WaapOpenRedirectPolicy.cc new file mode 100755 index 0000000..66c39bc --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapOpenRedirectPolicy.cc @@ -0,0 +1,36 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapOpenRedirectPolicy.h" +#include "Waf2Util.h" +#include + +namespace Waap { +namespace OpenRedirect { + +Policy::Policy() +: +enable(false), +enforce(false) +{ +} + +bool +Policy::operator==(const Policy &other) const +{ + return enable == other.enable && + enforce == other.enforce; +} + +} +} diff --git a/components/security_apps/waap/waap_clib/WaapOpenRedirectPolicy.h b/components/security_apps/waap/waap_clib/WaapOpenRedirectPolicy.h new file mode 100755 index 0000000..824e06a --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapOpenRedirectPolicy.h @@ -0,0 +1,52 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include +#include +#include +#include "debug.h" + +namespace Waap { +namespace OpenRedirect { + +struct Policy { + Policy(); + + template + Policy(_A &ar) + : + enable(false), + enforce(false) + { + std::string level; + ar(cereal::make_nvp("openRedirect", level)); + level = boost::algorithm::to_lower_copy(level); + if (level == "detect") { + enable = true; + } + else if (level == "prevent") { + enable = true; + enforce = true; + } + } + + bool operator==(const Policy &other) const; + + bool enable; + bool enforce; +}; + +} +} diff --git a/components/security_apps/waap/waap_clib/WaapOverride.cc b/components/security_apps/waap/waap_clib/WaapOverride.cc new file mode 100755 index 0000000..87af2ae --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapOverride.cc @@ -0,0 +1,81 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapOverride.h" +#include "Waf2Util.h" + +USE_DEBUG_FLAG(D_WAAP); + +namespace Waap { +namespace Override { + +bool Match::operator==(const Match &other) const +{ + return (m_op == other.m_op) && + (m_operand1 == other.m_operand1) && + (m_operand2 == other.m_operand2) && + (m_tag == other.m_tag) && + Waap::Util::compareObjects(m_valueRegex, other.m_valueRegex) && + m_cidr == other.m_cidr && + m_isCidr == other.m_isCidr; +} + +Behavior::Behavior() +: m_action(""), m_log(""), m_sourceIdentifier("") +{ +} + +bool Behavior::operator==(const Behavior &other) const +{ + return (m_action == other.m_action) && (m_log == other.m_log) && (m_sourceIdentifier == other.m_sourceIdentifier); +} + +const std::string & Behavior::getAction() const +{ + return m_action; +} + +const std::string& Behavior::getLog() const +{ + return m_log; +} + +const std::string& Behavior::getSourceIdentifier() const +{ + return m_sourceIdentifier; +} + +bool Rule::operator==(const Rule &other) const +{ + return (m_match == other.m_match) && + (m_isChangingRequestData == other.m_isChangingRequestData) && + (m_behaviors == other.m_behaviors); +} + +bool Policy::operator==(const Policy &other) const +{ + return m_RequestOverrides == other.m_RequestOverrides && + m_ResponseOverrides == other.m_ResponseOverrides; +} + +State::State() : + bForceBlock(false), + bForceException(false), + bIgnoreLog(false), + bSourceIdentifierOverride(false), + sSourceIdentifierMatch("") +{ +} + +} +} diff --git a/components/security_apps/waap/waap_clib/WaapOverride.h b/components/security_apps/waap/waap_clib/WaapOverride.h new file mode 100755 index 0000000..592a57c --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapOverride.h @@ -0,0 +1,345 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include +#include +#include +#include +#include +#include +#include "debug.h" +#include "CidrMatch.h" + +USE_DEBUG_FLAG(D_WAAP_OVERRIDE); + +namespace Waap { +namespace Override { +using boost::algorithm::to_lower_copy; + +class Match { +public: + bool operator==(const Match &other) const; + + template + void serialize(_A &ar) { + // Read the value of "op" + ar(cereal::make_nvp("operator", m_op)); + m_op = to_lower_copy(m_op); + m_isCidr = false; + m_value = ""; + + if (m_op == "basic") { + // If op == "BASIC" - read numeric value + ar(cereal::make_nvp("tag", m_tag)); + m_tag = to_lower_copy(m_tag); + + // The name "value" here is misleading. The real meaning is "regex pattern string" + ar(cereal::make_nvp("value", m_value)); + + if (m_tag == "sourceip" || m_tag == "sourceidentifier") { + m_isCidr = Waap::Util::isCIDR(m_value, m_cidr); + } + + if (!m_isCidr) { + // regex build may throw boost::regex_error + m_valueRegex = nullptr; + try { + m_valueRegex = std::make_shared(m_value); + } + catch (const boost::regex_error &err) { + dbgDebug(D_WAAP_OVERRIDE) << "Waap::Override::Match(): Failed to compile regex pattern '" << + m_value << "' on position " << err.position() << ". Reason: '" << err.what() << "'"; + } + } + } + else { + // If op is "AND" or "OR" - get two operands + if (m_op == "and" || m_op == "or") { + m_operand1 = std::make_shared(); + ar(cereal::make_nvp("operand1", *m_operand1)); + m_operand2 = std::make_shared(); + ar(cereal::make_nvp("operand2", *m_operand2)); + } + else if (m_op == "not") { + // If op is "NOT" get one operand + m_operand1 = std::make_shared(); + ar(cereal::make_nvp("operand1", *m_operand1)); + } + } + } + + template + bool match(TestFunctor testFunctor) const { + if (m_op == "basic" && m_isCidr) { + bool result = testFunctor(m_tag, m_cidr); + dbgTrace(D_WAAP_OVERRIDE) << "Override matching CIDR: " << m_value << " result: " << result; + return result; + } + else if (m_op == "basic" && m_valueRegex) { + bool result = testFunctor(m_tag, *m_valueRegex); + dbgTrace(D_WAAP_OVERRIDE) << "Override matching regex: " << m_value << " result: " << result; + return result; + } + if (m_op == "and") { + bool result = m_operand1->match(testFunctor) && m_operand2->match(testFunctor); + dbgTrace(D_WAAP_OVERRIDE) << "Override matching logical AND result: " << result; + return result; + } + if (m_op == "or") { + bool result = m_operand1->match(testFunctor) || m_operand2->match(testFunctor); + dbgTrace(D_WAAP_OVERRIDE) << "Override matching logical OR result: " << result; + return result; + } + if (m_op == "not") { + bool result = !m_operand1->match(testFunctor); + dbgTrace(D_WAAP_OVERRIDE) << "Override matching logical NOT result: " << result; + return result; + } + + // unknown operator. this should not occur + dbgDebug(D_WAAP_OVERRIDE) << "Invalid override operator " << m_op; + return false; + } + +private: + std::string m_op; + std::shared_ptr m_operand1; + std::shared_ptr m_operand2; + std::string m_tag; + std::string m_value; + std::shared_ptr m_valueRegex; + Waap::Util::CIDRData m_cidr; + bool m_isCidr; +}; + +class Behavior +{ +public: + Behavior(); + bool operator==(const Behavior &other) const; + + template + void serialize(_A &ar) { + try + { + ar(cereal::make_nvp("action", m_action)); + m_action = to_lower_copy(m_action); + } + catch (std::runtime_error& e) { + ar.setNextName(nullptr); + m_action = ""; + } + + try + { + ar(cereal::make_nvp("log", m_log)); + m_log = to_lower_copy(m_log); + } + catch (const std::runtime_error& e) { + ar.setNextName(nullptr); + m_log = ""; + } + + try + { + ar(cereal::make_nvp("httpSourceId", m_sourceIdentifier)); + } + catch (const std::runtime_error & e) { + ar.setNextName(nullptr); + m_sourceIdentifier = ""; + } + + if (!m_log.size() && !m_action.size() && !m_sourceIdentifier.size()) + { + dbgDebug(D_WAAP_OVERRIDE) << "Override does not contain any relevant action"; + } + } + + const std::string &getAction() const; + const std::string &getLog() const; + const std::string &getSourceIdentifier() const; +private: + std::string m_action; + std::string m_log; + std::string m_sourceIdentifier; +}; + +class Rule { +public: + bool operator==(const Rule &other) const; + + template + void serialize(_A &ar) { + try { + ar(cereal::make_nvp("id", m_id)); + } + catch (const cereal::Exception &e) + { + dbgTrace(D_WAAP_OVERRIDE) << "An override rule has no id."; + m_id.clear(); + } + + ar(cereal::make_nvp("parsedMatch", m_match)); + ar(cereal::make_nvp("parsedBehavior", m_behaviors)); + + m_isChangingRequestData = false; + + for (std::vector::const_iterator it = m_behaviors.begin(); + it != m_behaviors.end(); + ++it) + { + const Behavior& behavior = *it; + if (!behavior.getSourceIdentifier().empty()) // this rule changes data in request itself + { + m_isChangingRequestData = true; + break; + } + } + } + + template + void match(TestFunctor testFunctor, std::vector &matchedBehaviors, + std::set &matchedOverrideIds) const + { + if (m_match.match(testFunctor)) { + // extend matchedBehaviors list with all behaviors on this rule + dbgTrace(D_WAAP_OVERRIDE) << "Override rule matched. Adding " << m_behaviors.size() << " new behaviors:"; + std::string overrideId = getId(); + if (!overrideId.empty()) { + matchedOverrideIds.insert(overrideId); + } + for (const Behavior &behavior : m_behaviors) { + dbgTrace(D_WAAP_OVERRIDE) << "Behavior: action='" << behavior.getAction() << "', log='" << + behavior.getLog() << "', sourceIdentifier='" << behavior.getSourceIdentifier() << "'"; + matchedBehaviors.push_back(behavior); + } + return; + } + dbgTrace(D_WAAP_OVERRIDE) << "Rule not matched"; + } + + bool isChangingRequestData() const { + return m_isChangingRequestData; + } + + const std::string &getId() const { + return m_id; + } + +private: + Match m_match; + bool m_isChangingRequestData; + std::vector m_behaviors; + std::string m_id; +}; + +class Policy { +public: + template + Policy(_A &ar) { + std::vector rules; + ar(cereal::make_nvp("overrides", rules)); + + for (std::vector::const_iterator it = rules.begin(); it != rules.end(); ++it) { + const Waap::Override::Rule& rule = *it; + if (rule.isChangingRequestData()) + { + m_RequestOverrides.push_back(rule); + } + else + { + m_ResponseOverrides.push_back(rule); + } + } + } + + bool operator==(const Policy &other) const; + + template + void match(TestFunctor &testFunctor, std::vector &matchedBehaviors, bool requestOverrides, + std::set &matchedOverrideIds) const + { + // Run all rules and collect matched behaviors + + const std::vector& rules = requestOverrides ? m_RequestOverrides : m_ResponseOverrides; + dbgTrace(D_WAAP_OVERRIDE) << "Start matching override rules ..."; + for (const Waap::Override::Rule &rule : rules) { + dbgTrace(D_WAAP_OVERRIDE) << "Matching override rule ..."; + rule.match(testFunctor, matchedBehaviors, matchedOverrideIds); + } + dbgTrace(D_WAAP_OVERRIDE) << "Finished matching override rules."; + } + +private: + std::vector m_RequestOverrides; //overrides that change request data + std::vector m_ResponseOverrides; //overrides that change response/log data +}; + +struct State { + // whether to force block regardless of stage2 response (and even if bSendRequest and/or bSendResponse are false) + bool bForceBlock; + // exception (allow) was matched, so this request won't be blocked. + bool bForceException; + // overrides decision in case log should be ignored + bool bIgnoreLog; + // user identfier override to be applied + bool bSourceIdentifierOverride; + std::string sSourceIdentifierMatch; + + State(); + + // Compute overrides from override policy + template + void applyOverride(const Waap::Override::Policy &policy, Functor functor, + std::set &matchedOverrideIds, bool requestOverrides) + { + // Collect all behaviors from matched rules + std::vector matchedBehaviors; + policy.match(functor, matchedBehaviors, requestOverrides, matchedOverrideIds); + + dbgTrace(D_WAAP_OVERRIDE) << "applyOverride(): " << matchedBehaviors.size() << " detected override actions"; + + // Apply all detected behaviors + for (auto &matchedBehavior : matchedBehaviors) { + dbgTrace(D_WAAP_OVERRIDE) << "applyOverride(): found override action: " << matchedBehavior.getAction(); + if (matchedBehavior.getAction() == "accept") { + dbgTrace(D_WAAP_OVERRIDE) << "applyOverride(): setting bForceException due to override behavior."; + bForceException = true; + } + else if (matchedBehavior.getAction() == "reject") { + dbgTrace(D_WAAP_OVERRIDE) << "applyOverride(): setting bForceBlock due to override behavior."; + bForceBlock = true; + } + + if (matchedBehavior.getLog() == "ignore") + { + dbgTrace(D_WAAP_OVERRIDE) << "applyOverride(): setting bIgnoreLog due to override behavior."; + bIgnoreLog = true; + } + + sSourceIdentifierMatch = matchedBehavior.getSourceIdentifier(); + if (sSourceIdentifierMatch.size()) + { + dbgTrace(D_WAAP_OVERRIDE) << "applyOverride(): setting bSourceIdentifier -" + << "Override due to override behavior: " + << sSourceIdentifierMatch.c_str(); + bSourceIdentifierOverride = true; + } + } + } +}; + +} +} diff --git a/components/security_apps/waap/waap_clib/WaapOverrideFunctor.cc b/components/security_apps/waap/waap_clib/WaapOverrideFunctor.cc new file mode 100755 index 0000000..94bb7bd --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapOverrideFunctor.cc @@ -0,0 +1,108 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include "WaapOverrideFunctor.h" +#include "Waf2Engine.h" +#include "CidrMatch.h" +#include "agent_core_utilities.h" +#include "debug.h" + +USE_DEBUG_FLAG(D_WAAP_OVERRIDE); + +WaapOverrideFunctor::WaapOverrideFunctor(Waf2Transaction& waf2Transaction) :waf2Transaction(waf2Transaction) +{ +} + +bool WaapOverrideFunctor::operator()(const std::string& tag, const Waap::Util::CIDRData& value) { + if (tag == "sourceip") { + dbgDebug(D_WAAP_OVERRIDE) << "Remote IP Address : " << waf2Transaction.getRemoteAddr() << " CIDR: " << + value.cidrString; + std::string sourceIp = waf2Transaction.getRemoteAddr(); + // match sourceIp against the cidr + return Waap::Util::cidrMatch(sourceIp, value); + } + else if (tag == "sourceidentifier") { + dbgDebug(D_WAAP_OVERRIDE) << "Remote IP Address : " << waf2Transaction.getRemoteAddr() << " CIDR: " << + value.cidrString; + std::string sourceIp = waf2Transaction.getSourceIdentifier(); + // match source against the cidr + return Waap::Util::cidrMatch(sourceIp, value); + } + + return false; +} + +bool WaapOverrideFunctor::operator()(const std::string& tag, const boost::regex& rx) +{ + boost::cmatch what; + try { + if (tag == "url") { + return NGEN::Regex::regexMatch(__FILE__, __LINE__, waf2Transaction.getUriStr().c_str(), what, rx); + } + else if (tag == "hostname") { + return NGEN::Regex::regexMatch(__FILE__, __LINE__, waf2Transaction.getHost().c_str(), what, rx); + } + else if (tag == "sourceidentifier") { + return NGEN::Regex::regexMatch( + __FILE__, + __LINE__, + waf2Transaction.getSourceIdentifier().c_str(), + what, + rx + ); + } + else if (tag == "keyword") { + for (const std::string& keywordStr : waf2Transaction.getKeywordMatches()) { + if (NGEN::Regex::regexMatch(__FILE__, __LINE__, keywordStr.c_str(), what, rx)) { + return true; + } + } + return false; + } + else if (tag == "paramname" || tag == "paramName") { + for (const DeepParser::KeywordInfo& keywordInfo : waf2Transaction.getKeywordInfo()) { + if (NGEN::Regex::regexMatch(__FILE__, __LINE__, keywordInfo.getName().c_str(), what, rx)) { + return true; + } + } + if (NGEN::Regex::regexMatch(__FILE__, __LINE__, waf2Transaction.getParamKey().c_str(), what, rx)) { + return true; + } + if (NGEN::Regex::regexMatch(__FILE__, __LINE__, waf2Transaction.getParam().c_str(), what, rx)) { + return true; + } + return false; + } + else if (tag == "paramvalue" || tag == "paramValue") { + for (const DeepParser::KeywordInfo& keywordInfo : waf2Transaction.getKeywordInfo()) { + if (NGEN::Regex::regexMatch(__FILE__, __LINE__, keywordInfo.getValue().c_str(), what, rx)) { + return true; + } + } + if (NGEN::Regex::regexMatch(__FILE__, __LINE__, waf2Transaction.getSample().c_str(), what, rx)) { + return true; + } + return false; + } + } + catch (std::runtime_error & e) { + dbgDebug(D_WAAP_OVERRIDE) << "RegEx match for tag " << tag << " failed due to: " << e.what(); + return false; + } + + // Unknown tag: should not occur + dbgWarning(D_WAAP) << "Invalid override tag:" << tag; + return false; +} diff --git a/components/security_apps/waap/waap_clib/WaapOverrideFunctor.h b/components/security_apps/waap/waap_clib/WaapOverrideFunctor.h new file mode 100755 index 0000000..13d76ef --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapOverrideFunctor.h @@ -0,0 +1,35 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +namespace Waap { + namespace Util { + struct CIDRData; // forward decleration + } +} + +class Waf2Transaction; + +// Functor used to match Override rules against request data +class WaapOverrideFunctor { +public: + WaapOverrideFunctor(Waf2Transaction& waf2Transaction); + + bool operator()(const std::string& tag, const Waap::Util::CIDRData& value); + + bool operator()(const std::string& tag, const boost::regex& rx); + +private: + Waf2Transaction& waf2Transaction; +}; diff --git a/components/security_apps/waap/waap_clib/WaapParameters.cc b/components/security_apps/waap/waap_clib/WaapParameters.cc new file mode 100755 index 0000000..c8e70da --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapParameters.cc @@ -0,0 +1,35 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapParameters.h" + +using namespace Waap::Parameters; + +bool WaapParameters::operator==(const WaapParameters &other) const +{ + return m_paramMap == other.m_paramMap; +} + +ParamMap WaapParameters::getParamsMap() const +{ + return m_paramMap; +} + +Value WaapParameters::getParamVal(Parameter key, Value defaultVal) +{ + if (m_paramMap.find(key) == m_paramMap.end()) + { + return defaultVal; + } + return m_paramMap[key]; +} diff --git a/components/security_apps/waap/waap_clib/WaapParameters.h b/components/security_apps/waap/waap_clib/WaapParameters.h new file mode 100755 index 0000000..c7c5c12 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapParameters.h @@ -0,0 +1,45 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include +#include +#include + + +namespace Waap { + namespace Parameters { + typedef std::string Parameter; + typedef std::string Value; + typedef std::unordered_map ParamMap; + + class WaapParameters + { + public: + template + WaapParameters(_A& ar) + { + ar(cereal::make_nvp("waapParameters", m_paramMap)); + } + + bool operator==(const WaapParameters &other) const; + + ParamMap getParamsMap() const; + Value getParamVal(Parameter key, Value defaultVal); + private: + ParamMap m_paramMap; + }; + + } +} diff --git a/components/security_apps/waap/waap_clib/WaapRegexPreconditions.cc b/components/security_apps/waap/waap_clib/WaapRegexPreconditions.cc new file mode 100755 index 0000000..f4bca8e --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapRegexPreconditions.cc @@ -0,0 +1,341 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapRegexPreconditions.h" +#include "Waf2Util.h" +#include "debug.h" +#include + +USE_DEBUG_FLAG(D_WAAP_REGEX); + +namespace Waap { + const RegexPreconditions::WordIndex RegexPreconditions::emptyWordIndex = 0; + + RegexPreconditions::RegexPreconditions(const picojson::value::object &jsObj, bool &error) + { + // Register empty string work under known index + registerWord(""); + + // The key should always be there unless data file is corrupted (but there's a unit test that tests exactly + // that!) + if (jsObj.find("preconditions") == jsObj.end()) { + dbgError(D_WAAP_REGEX) << "Error loading regex preconditions (signatures data file corrupt?)..."; + error = true; + return; + } + + if (jsObj.find("precondition_keys") == jsObj.end()) { + dbgError(D_WAAP_REGEX) << "Error loading regex precondition sets (signatures data file corrupt?)..."; + error = true; + return; + } + + auto preconditions = jsObj.at("preconditions").get(); + + // Build full list of words to load into aho-corasick pattern matcher + dbgTrace(D_WAAP_REGEX) << "Loading regex precondition_keys into Aho-Corasick pattern matcher..."; + + auto preconditionKeys = jsObj.at("precondition_keys").get(); + std::set pmPatterns; + + for (const auto &preconditionKey : preconditionKeys) { + std::string wordStr(preconditionKey.get()); + + // Do not load the "empty" word into Aho-Corasick. It's meaningless and Aho prepare() call would fail. + if (wordStr.empty()) { + continue; + } + + WordIndex wordIndex = registerWord(wordStr); + pmPatterns.insert(PMPattern(wordStr, false, false, wordIndex)); + } + + // Initialize the aho-corasick pattern matcher with the patterns + Maybe pmHookStatus = m_pmHook.prepare(pmPatterns); + + if (!pmHookStatus.ok()) { + dbgError(D_WAAP_REGEX) << "Aho-Corasick engine failed to load!"; + error = true; + return; + } + + dbgTrace(D_WAAP_REGEX) << "Aho-Corasick engine loaded."; + + // Loop over pre-conditions (rules) and load them + dbgTrace(D_WAAP_REGEX) << "Loading regex preconditions..."; + + for (const auto &precondition : preconditions) + { + // Each precondition consists of an aho-corasick pattern matcher word as a key and list of actions + // (for that word) - as a value. + const std::string wordStr = precondition.first; + + // Information from the "empty string"" word is not required by the engine to operate + if (wordStr.empty()) { + continue; + } + + WordIndex wordIndex = registerWord(wordStr); + + if (boost::algorithm::ends_with(wordStr, "_napost_napre")) { + WordIndex baseWordIndex = registerWord(wordStr.substr(0, wordStr.size() - strlen("_napost_napre"))); + m_pmWordInfo[baseWordIndex].napostNapreWordIndex = wordIndex; + m_pmWordInfo[wordIndex].baseWordIndex = baseWordIndex; + } + else if (boost::algorithm::ends_with(wordStr, "_napost")) { + WordIndex baseWordIndex = registerWord(wordStr.substr(0, wordStr.size() - strlen("_napost"))); + m_pmWordInfo[baseWordIndex].napostWordIndex = wordIndex; + m_pmWordInfo[wordIndex].baseWordIndex = baseWordIndex; + } + else if (boost::algorithm::ends_with(wordStr, "_napre")) { + WordIndex baseWordIndex = registerWord(wordStr.substr(0, wordStr.size() - strlen("_napre"))); + m_pmWordInfo[baseWordIndex].napreWordIndex = wordIndex; + m_pmWordInfo[wordIndex].baseWordIndex = baseWordIndex; + } + + // Load actions + const auto &jsActionsList = precondition.second.get(); + + for (const auto &jsAction : jsActionsList) { + const auto &action = jsAction.get(); + + if (action.empty()) { + continue; + } + + // The first item in the Action json object (it's a tuple of 1 or more items) is an action type string. + const std::string actionType = action[0].get(); + + // There are currently three action types: + // 1. "regex" - allow specific regex to be scanned when the Aho-Corasick word is detected + // 2. "set" - specify another "prefix" (string) to be enabled when the Aho-Corasick word is detected. + // if at least one prefix is enabled - it will trigger one or more other regexes. + // 3. "and_condition" - specify (comma-separated) sorted list of "prefixes" (in one string). + // all of these prefixes should come together in order to complete a set to match a + // condition and enable one or more other regexes. + if (actionType == "regex" && action.size() >= 3) { + const std::string regexPattern = action[1].get(); + if (m_regexToWordMap.find(regexPattern) != m_regexToWordMap.end() && + m_regexToWordMap[regexPattern] != wordIndex) + { + dbgError(D_WAAP_REGEX) << "ERROR: trying to overwrite m_regexToWordMap. pattern='" << + regexPattern << "'. Old wordIndex='" << m_regexToWordMap[regexPattern] << "' new word='" + << wordStr << "' (wordIndex=" << wordIndex << ")"; + error = true; + return; + } + + std::string flags = action[2].get(); + + if (flags == "_noregex") { + // Add regex pattern to set of "noRegex" patterns + m_noRegexPatterns.insert(regexPattern); + } + + m_regexToWordMap[regexPattern] = wordIndex; + } + else if (actionType == "set" && action.size() >= 2) { + const std::string setValueStr = action[1].get(); + WordIndex setValueIndex = registerWord(setValueStr); + std::vector &prefixSet = m_wordToPrefixSet[wordIndex]; + if (std::find(prefixSet.begin(), prefixSet.end(), + setValueIndex) == prefixSet.end()) { + prefixSet.push_back(setValueIndex); + } + } + else if (actionType == "and_condition" && action.size() >= 2) { + const std::string groupValueStr = action[1].get(); + WordIndex groupValueIndex = registerWord(groupValueStr); + size_t expectedCount = static_cast(std::stoi(groupValueStr)); + auto value(std::make_pair(groupValueIndex, expectedCount)); + std::vector> &prefixGroup = m_wordToPrefixGroup[wordIndex]; + if (std::find(prefixGroup.begin(), prefixGroup.end(), + value) == prefixGroup.end()) { + prefixGroup.push_back(value); + } + } + } + } + + dbgTrace(D_WAAP_REGEX) << "Aho-corasick pattern matching engine initialized!"; + } + + bool Waap::RegexPreconditions::isNoRegexPattern(const std::string &pattern) const + { + return m_noRegexPatterns.find(pattern) != m_noRegexPatterns.end(); + } + + const std::string &Waap::RegexPreconditions::getWordStrByWordIndex(WordIndex wordIndex) const + { + WordIndex baseWordIndex = m_pmWordInfo[wordIndex].baseWordIndex; + + if (baseWordIndex != Waap::RegexPreconditions::emptyWordIndex) { + return m_pmWordInfo[baseWordIndex].wordStr; + } + + return m_pmWordInfo[wordIndex].wordStr; + } + + // Check that the regex pattern (string) is known to be related to an Aho-Corasick word/prefix + // Returns empty string if not found, or the Aho-Corasick/prefix string otherwise. + // This function is called during each Regex object creation and helps to pre-compute data required for a fast + // lookup later during traffic processing. + Waap::RegexPreconditions::WordIndex RegexPreconditions::getWordByRegex(const std::string ®exPattern) const + { + const auto &found = m_regexToWordMap.find(regexPattern); + + if (found != m_regexToWordMap.end()) { + return found->second; + } + + return Waap::RegexPreconditions::emptyWordIndex; + } + + void RegexPreconditions::processWord(RegexPreconditions::PmWordSet &wordsSet, WordIndex wordIndex) const + { + const auto &found = m_wordToPrefixSet.find(wordIndex); + + if (found != m_wordToPrefixSet.end()) { + for (const auto &prefixIndex : found->second) { + // One of the items in the "OR" condition - add the OR prefix to the wordsSet + wordsSet.insert(prefixIndex); + } + } + + // Add words from the Aho Corasick scanner + wordsSet.insert(wordIndex); + } + + inline bool isRegexWordChar(u_char c) { + return Waap::Util::isAlphaAsciiFast(c) || isdigit(c) || '_' == c; + } + + void RegexPreconditions::pass1(RegexPreconditions::PmWordSet &wordsSet, Buffer &&buffer) const + { + dbgTrace(D_WAAP_REGEX) << "Rules pass #1: collect OR sets"; + + m_pmHook.scanBufWithOffsetLambda(buffer, [this, &wordsSet, &buffer] + (u_int endMatchOffset, const PMPattern &pmPattern) + { + uint offset = endMatchOffset + 1 - pmPattern.size(); // reported offset points to last character of a match + + // Extract the word index from the PMPattern object (we do not need the string part of it) + WordIndex wordIndex = pmPattern.getIndex(); + + bool regexWordBefore = (offset != 0) && + (isRegexWordChar(buffer.data()[offset - 1])); + bool regexWordAfter = (offset + pmPattern.size() < buffer.size()) && + (isRegexWordChar(buffer.data()[offset + pmPattern.size()])); + + processWord(wordsSet, wordIndex); + + // Compute additional constraints ([!\w] before, [!\w] after, [!\w] aroung the match ...) + WordIndex napreWordIndex = m_pmWordInfo[wordIndex].napreWordIndex; + WordIndex napostWordIndex = m_pmWordInfo[wordIndex].napostWordIndex; + WordIndex napostNapreWordIndex = m_pmWordInfo[wordIndex].napostNapreWordIndex; + + if (!regexWordBefore && regexWordAfter) { + if (napreWordIndex != emptyWordIndex) { + processWord(wordsSet, napreWordIndex); + } + } + else if (regexWordBefore && !regexWordAfter) { + if (napostWordIndex != emptyWordIndex) { + processWord(wordsSet, napostWordIndex); + } + } + else if (!regexWordBefore && !regexWordAfter) { + if (napreWordIndex != emptyWordIndex) { + processWord(wordsSet, napreWordIndex); + } + + if (napostWordIndex != emptyWordIndex) { + processWord(wordsSet, napostWordIndex); + } + + if (napostNapreWordIndex != emptyWordIndex) { + processWord(wordsSet, napostNapreWordIndex); + } + } + }); + } + + void RegexPreconditions::pass2(RegexPreconditions::PmWordSet &wordsSet) const + { + dbgTrace(D_WAAP_REGEX) << "Rules pass #2: collect AND groups"; + + std::unordered_map> allGroups; + std::vector prefixes; + + for (WordIndex wordIndex : wordsSet) { + // find in wordToPrefixGroup map + const auto &found = m_wordToPrefixGroup.find(wordIndex); + + if (found != m_wordToPrefixGroup.end()) { + for (const auto &prefixCountPair : found->second) { + WordIndex prefixIndex = prefixCountPair.first; + size_t expectedCount = prefixCountPair.second; + + auto found = allGroups.find(prefixIndex); + size_t actualWordCount = 1; + + if (found == allGroups.end()) { + allGroups.emplace(prefixIndex, std::set{wordIndex}); + } + else { + found->second.insert(wordIndex); + actualWordCount = found->second.size(); + } + + if (actualWordCount == expectedCount) { + // Full "AND" condition collected succesfully - add the AND prefixCountPair to the wordsSet + prefixes.push_back(prefixIndex); + } + } + } + } + + for (const auto &prefixIndex : prefixes) { + wordsSet.insert(prefixIndex); + } + } + + // This function scans the buffer with Aho-Corasick scanner and adds all the words found into wordsSet + // It then continues and runs two pass algorithm to compute OR and AND conditions over a prefixes data. + // The prefix strings are also added to the wordsSet and are looked up in the same database. + void RegexPreconditions::pmScan(Buffer &&buffer, RegexPreconditions::PmWordSet &wordsSet) const + { + wordsSet.clear(); + pass1(wordsSet, std::move(buffer)); + pass2(wordsSet); + // The empty string key contains all regexes that should always be scanned + wordsSet.insert(Waap::RegexPreconditions::emptyWordIndex); + } + + // Get known wordIndex by wordStr, or allocate a new wordIndex for words yet unknown + Waap::RegexPreconditions::WordIndex RegexPreconditions::registerWord(const std::string &wordStr) + { + const auto &found = m_wordStrToIndex.find(wordStr); + if (found != m_wordStrToIndex.end()) { + return found->second; + } + else { + WordIndex wordIndex = m_pmWordInfo.size(); + m_wordStrToIndex[wordStr] = wordIndex; // index of the new element that will be added below... + WordInfo wordInfo; + wordInfo.wordStr = wordStr; + m_pmWordInfo.push_back(wordInfo); + return wordIndex; + } + } +} diff --git a/components/security_apps/waap/waap_clib/WaapRegexPreconditions.h b/components/security_apps/waap/waap_clib/WaapRegexPreconditions.h new file mode 100755 index 0000000..f3c2c69 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapRegexPreconditions.h @@ -0,0 +1,89 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __WAAP_REGEX_PRECONDITIONS_H__ +#define __WAAP_REGEX_PRECONDITIONS_H__ + +#include "picojson.h" +#include "pm_hook.h" +#include "i_pm_scan.h" +#include +#include +#include +#include + +namespace Waap { + class RegexPreconditions + { + public: + typedef size_t WordIndex; + static const WordIndex emptyWordIndex; // special word index used to index the "impossible" empty word + private: + // Maps regex pattern string to Aho-Coraick pattern matcher word + typedef std::unordered_map RegexToWordMap; + // Maps Aho-Corasick pattern word to list of "prefixes" (special tags used to implement OR and AND semantics) + typedef std::unordered_map> WordToPrefixSet; + typedef std::unordered_map>> WordToPrefixGroup; + public: + typedef std::unordered_set PmWordSet; + + // The constructor builds internal data from Json object. Once built - the object becomes read-only. + RegexPreconditions(const picojson::value::object &jsObj, bool &error); + bool isNoRegexPattern(const std::string &pattern) const; + const std::string &getWordStrByWordIndex(WordIndex wordIndex) const; + Waap::RegexPreconditions::WordIndex getWordByRegex(const std::string &pattern) const; + // Run aho-corasick scan on a sample followed by "set" and "and_condition" rules. Returns set of words + // that can be used to speed up following calls to Regex::findAllMatches() on the same sample. + void pmScan(Buffer &&buffer, RegexPreconditions::PmWordSet &allSets) const; + + private: + void processWord(RegexPreconditions::PmWordSet &wordsSet, WordIndex wordIndex) const; + void pass1(RegexPreconditions::PmWordSet &wordsSet, Buffer &&buffer) const; + void pass2(RegexPreconditions::PmWordSet &wordsSet) const; + + RegexToWordMap m_regexToWordMap; + // For each aho-corasick word - hold a list of "prefixes" which are in OR relationship between them (at least + // one must match in order to trigger a condition on a prefix) + WordToPrefixSet m_wordToPrefixSet; + // For each aho-corasick word - hold a list of "prefixes" which are in AND relationship between them (all must + // be detected in order to trigger a condition on a prefix) + WordToPrefixGroup m_wordToPrefixGroup; + // Aho-Corasick pattern matcher object + PMHook m_pmHook; + + struct WordInfo { + WordIndex napostNapreWordIndex; + WordIndex napostWordIndex; + WordIndex napreWordIndex; + WordIndex baseWordIndex; + std::string wordStr; + + WordInfo() + : + napostNapreWordIndex(emptyWordIndex), + napostWordIndex(emptyWordIndex), + napreWordIndex(emptyWordIndex), + baseWordIndex(0), + wordStr() + { + } + }; + + WordIndex registerWord(const std::string &wordStr); + std::vector m_pmWordInfo; + std::map m_wordStrToIndex; // TODO:: remove this into throwaway object, no need to keep + std::set m_noRegexPatterns; // patterns that require no regex matching (Aho Corasick is enough) + }; +} + +#endif // __WAAP_REGEX_PRECONDITIONS_H__ diff --git a/components/security_apps/waap/waap_clib/WaapResponseInjectReasons.cc b/components/security_apps/waap/waap_clib/WaapResponseInjectReasons.cc new file mode 100644 index 0000000..c88d233 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapResponseInjectReasons.cc @@ -0,0 +1,91 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapResponseInjectReasons.h" +#include "debug.h" +#include + +USE_DEBUG_FLAG(D_WAAP); + +namespace Waap { + +ResponseInjectReasons::ResponseInjectReasons() +: +csrf(false), +antibot(false), +securityHeaders(false) +{ +} + +void +ResponseInjectReasons::clear() +{ + dbgTrace(D_WAAP) << "ResponseInjectReasons::clear()"; + setCsrf(false); + setAntibot(false); + setSecurityHeaders(false); +} + +bool +ResponseInjectReasons::shouldInject() const +{ + dbgTrace(D_WAAP) << "ResponseInjectReasons::shouldInject():" << + " AntiBot= " << antibot << + " CSRF= " << csrf << + " SecurityHeaders= " << securityHeaders; + return csrf || antibot || securityHeaders; +} + +void +ResponseInjectReasons::setAntibot(bool flag) +{ + dbgTrace(D_WAAP) << "Change ResponseInjectReasons(Antibot) " << antibot << " to " << flag; + antibot = flag; +} + +void +ResponseInjectReasons::setCsrf(bool flag) +{ + dbgTrace(D_WAAP) << "Change ResponseInjectReasons(CSRF) " << csrf << " to " << flag; + csrf = flag; +} + +void +ResponseInjectReasons::setSecurityHeaders(bool flag) +{ + dbgTrace(D_WAAP) << "Change ResponseInjectReasons(Security Headers) " << securityHeaders << " to " << flag; + securityHeaders = flag; +} + +bool +ResponseInjectReasons::shouldInjectAntibot() const +{ + dbgTrace(D_WAAP) << "shouldInjectAntibot():: " << antibot; + return antibot; +} + +bool +ResponseInjectReasons::shouldInjectCsrf() const +{ + dbgTrace(D_WAAP) << "shouldInjectCsrf():: " << csrf; + return csrf; +} + +bool +ResponseInjectReasons::shouldInjectSecurityHeaders() const +{ + dbgTrace(D_WAAP) << "shouldInjectSecurityHeaders():: " << securityHeaders; + return securityHeaders; +} + +} diff --git a/components/security_apps/waap/waap_clib/WaapResponseInjectReasons.h b/components/security_apps/waap/waap_clib/WaapResponseInjectReasons.h new file mode 100644 index 0000000..af4e7ee --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapResponseInjectReasons.h @@ -0,0 +1,35 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +namespace Waap { + +class ResponseInjectReasons { +public: + ResponseInjectReasons(); + void clear(); + bool shouldInject() const; + void setAntibot(bool flag); + void setCsrf(bool flag); + void setSecurityHeaders(bool flag); + bool shouldInjectAntibot() const; + bool shouldInjectCsrf() const; + bool shouldInjectSecurityHeaders() const; +private: + bool csrf; + bool antibot; + bool securityHeaders; +}; + +} diff --git a/components/security_apps/waap/waap_clib/WaapResponseInspectReasons.cc b/components/security_apps/waap/waap_clib/WaapResponseInspectReasons.cc new file mode 100644 index 0000000..d626057 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapResponseInspectReasons.cc @@ -0,0 +1,79 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapResponseInspectReasons.h" +#include "debug.h" + +USE_DEBUG_FLAG(D_WAAP); + +namespace Waap { + +ResponseInspectReasons::ResponseInspectReasons() +: +openRedirect(false), +errorDisclosure(false), +errorLimiter(false), +rateLimiting(false), +collectResponseForLog(false) +{ +} + +bool +ResponseInspectReasons::shouldInspect() const +{ + dbgTrace(D_WAAP) << "ResponseInspectReasons::shouldInspect():" << + " OpenRedirect=" << openRedirect << + " ErrorDisclosure=" << errorDisclosure << + " RateLimiting=" << rateLimiting << + " ErrorLimiter=" << errorLimiter << + " collectResponseForLog=" << collectResponseForLog; + return openRedirect || errorDisclosure || rateLimiting || errorLimiter || collectResponseForLog; +} + +void +ResponseInspectReasons::setOpenRedirect(bool flag) +{ + dbgTrace(D_WAAP) << "Change ResponseInspectReasons(OpenRedirect) " << openRedirect << " to " << flag; + openRedirect = flag; +} + +void +ResponseInspectReasons::setErrorDisclosure(bool flag) +{ + dbgTrace(D_WAAP) << "Change ResponseInspectReasons(ErrorDisclosure) " << errorDisclosure << " to " << flag; + errorDisclosure = flag; +} + +void +ResponseInspectReasons::setRateLimiting(bool flag) +{ + dbgTrace(D_WAAP) << "Change ResponseInspectReasons(RateLimiting) " << rateLimiting << " to " << flag; + rateLimiting = flag; +} + +void +ResponseInspectReasons::setErrorLimiter(bool flag) +{ + dbgTrace(D_WAAP) << "Change ResponseInspectReasons(ErrorLimiter) " << errorLimiter << " to " << flag; + errorLimiter = flag; +} + +void +ResponseInspectReasons::setCollectResponseForLog(bool flag) +{ + dbgTrace(D_WAAP) << "Change ResponseInspectReasons(collectResponseForLog) " << collectResponseForLog << " to " << + flag; + collectResponseForLog = flag; +} + +} diff --git a/components/security_apps/waap/waap_clib/WaapResponseInspectReasons.h b/components/security_apps/waap/waap_clib/WaapResponseInspectReasons.h new file mode 100644 index 0000000..eb13c6c --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapResponseInspectReasons.h @@ -0,0 +1,35 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +namespace Waap { + +class ResponseInspectReasons { +public: + ResponseInspectReasons(); + bool shouldInspect() const; + void setOpenRedirect(bool flag); + void setErrorDisclosure(bool flag); + void setRateLimiting(bool flag); + void setErrorLimiter(bool flag); + void setCollectResponseForLog(bool flag); +private: + bool openRedirect; + bool errorDisclosure; + bool errorLimiter; + bool rateLimiting; + bool collectResponseForLog; +}; + +} diff --git a/components/security_apps/waap/waap_clib/WaapResultJson.cc b/components/security_apps/waap/waap_clib/WaapResultJson.cc new file mode 100644 index 0000000..e7aab48 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapResultJson.cc @@ -0,0 +1,255 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapResultJson.h" +#include "Waf2Engine.h" +#include "WaapAssetState.h" + +std::string buildWaapResultJson(Waf2ScanResult *m_scanResult, const Waf2Transaction &t, bool bSendResponse, + const std::string &normalizedUri, const std::string &uri, bool bForceBlock, + bool bForceException) +{ + auto hdr_pairs = t.getHdrPairs(); + auto notes = t.getNotes(); + auto scanResultKeywordCombinations = t.getKeywordsCombinations(); + auto keywordInfo = t.getKeywordInfo(); + auto kvPairs = t.getKvPairs(); + auto scoreArray = t.getScoreArray(); + + if (m_scanResult) { + Waap::Util::Yajl y; + { + Waap::Util::Yajl::Map root(y); + root.gen_key("data"); + { + Waap::Util::Yajl::Map data(y); + + data.gen_key("transaction"); + { + Waap::Util::Yajl::Map transaction(y); + transaction.gen_str("time", t.getLogTime()); + transaction.gen_integer("remote_port", t.getRemotePort()); + transaction.gen_str("remote_address", t.getRemoteAddr()); + std::string support_id = t.getTransactionIdStr(); + transaction.gen_str("support_id", support_id); + } + data.gen_key("request"); + { + Waap::Util::Yajl::Map request(y); + request.gen_str("method", t.getMethod()); + request.gen_str("uri", normalizedUri); + request.gen_str("orig_uri", uri); + request.gen_str("ct", t.getContentTypeStr()); + request.gen_key("headers"); + { + Waap::Util::Yajl::Map headers(y); + for (std::vector >::iterator it = hdr_pairs.begin(); + it != hdr_pairs.end(); + ++it) { + headers.gen_str(it->first, it->second); + } + } + } + data.gen_str("ct", t.getContentTypeStr()); + } + root.gen_key("res"); + { + Waap::Util::Yajl::Map res(y); + res.gen_str("param_location", m_scanResult->location); + res.gen_str("param_name", m_scanResult->param_name); + res.gen_str("line", m_scanResult->unescaped_line); + res.gen_key("keyword_matches"); + { + Waap::Util::Yajl::Array keyword_matches(y); + for (std::vector::iterator pM = m_scanResult->keyword_matches.begin(); + pM != m_scanResult->keyword_matches.end(); + ++pM) { + std::string& m = *pM; + keyword_matches.gen_str(m); + } + } + res.gen_key("ntags"); + { + Waap::Util::Yajl::Map ntags(y); + for (Waap::Util::map_of_stringlists_t::iterator pKv = m_scanResult->found_patterns.begin(); + pKv != m_scanResult->found_patterns.end(); + ++pKv) { + ntags.gen_key(pKv->first); + { + Waap::Util::Yajl::Array ntags_val(y); + for (std::vector::iterator pV = pKv->second.begin(); + pV != pKv->second.end(); + ++pV) { + ntags_val.gen_str(*pV); + } + } + } + } + res.gen_double("score", t.getScore()); + res.gen_key("scores_array"); + { + Waap::Util::Yajl::Array scores_array(y); + for (std::vector::iterator pScore = scoreArray.begin(); + pScore != scoreArray.end(); + ++pScore) { + scores_array.gen_double(*pScore); + } + } + res.gen_key("keyword_combinations"); + { + Waap::Util::Yajl::Array keyword_combinations_array(y); + for (std::vector::iterator pCombination = scanResultKeywordCombinations.begin(); + pCombination != scanResultKeywordCombinations.end(); + ++pCombination) { + keyword_combinations_array.gen_str(*pCombination); + } + } + } + + root.gen_bool("stage1_force_block", bForceBlock); + + if (bForceException) { + root.gen_bool("stage1_force_exception", bForceException); + } + + // TODO:: the output of these should be throttled to up to X per minute (or hour). + // Maybe throttling should be done elsewhere and flag should be present whether to + // output the data or not (or just assume i m_keywordInfo.size()==0 - don't output). + root.gen_key("k_api"); + { + Waap::Util::Yajl::Array k_api(y); + for (std::vector::const_iterator it = keywordInfo.begin(); + it != keywordInfo.end(); + ++it) { + const DeepParser::KeywordInfo& keywordInfo = *it; + Waap::Util::Yajl::Map k_api_kw(y); + k_api_kw.gen_str("type", keywordInfo.getType()); + k_api_kw.gen_str("name", keywordInfo.getName()); + k_api_kw.gen_str("value", keywordInfo.getValue()); + k_api_kw.gen_integer("len", keywordInfo.getValue().length()); + } + } + root.gen_key("x_kvs"); + { + Waap::Util::Yajl::Map x_kvs(y); + for (std::vector >::iterator it = kvPairs.begin(); + it != kvPairs.end(); + ++it) { + std::string& k = it->first; + std::string& v = it->second; + x_kvs.gen_str(k, v); + } + } + + root.gen_str("x_body", t.getRequestBody()); + if (!notes.empty()) { + root.gen_key("notes"); + Waap::Util::Yajl::Array jsNotes(y); + for (std::vector::const_iterator it = notes.begin(); it != notes.end(); ++it) { + jsNotes.gen_str(*it); + } + } + + root.gen_bool("send_response", bSendResponse); + root.gen_bool("login_url", false); + } + + return (bSendResponse ? "1" : "0") + y.get_json_str(); + } + else { + Waap::Util::Yajl y; + { + Waap::Util::Yajl::Map root(y); + root.gen_key("data"); + { + Waap::Util::Yajl::Map data(y); + data.gen_key("transaction"); + { + Waap::Util::Yajl::Map transaction(y); + transaction.gen_str("time", t.getLogTime()); + transaction.gen_integer("remote_port", t.getRemotePort()); + transaction.gen_str("remote_address", t.getRemoteAddr()); + std::string support_id = t.getTransactionIdStr(); + transaction.gen_str("support_id", support_id); + } + data.gen_key("request"); + { + Waap::Util::Yajl::Map request(y); + request.gen_str("method", t.getMethod()); + request.gen_str("uri", normalizedUri); + request.gen_str("orig_uri", uri); + request.gen_str("ct", t.getContentTypeStr()); + request.gen_key("headers"); + { + Waap::Util::Yajl::Map headers(y); + for (std::vector >::iterator it = hdr_pairs.begin(); + it != hdr_pairs.end(); + ++it) { + headers.gen_str(it->first, it->second); + } + } + } + data.gen_str("ct", t.getContentTypeStr()); + } + + root.gen_bool("stage1_force_block", bForceBlock); + + if (bForceException) { + root.gen_bool("stage1_force_exception", bForceException); + } + + // TODO:: the output of these should be throttled to up to X per minute (or hour). + // Maybe throttling should be done elsewhere and flag should be present whether to + // output the data or not (or just assume i m_keywordInfo.size()==0 - don't output). + root.gen_key("k_api"); + { + Waap::Util::Yajl::Array k_api(y); + for (std::vector::const_iterator it = keywordInfo.begin(); + it != keywordInfo.end(); + ++it) { + const DeepParser::KeywordInfo& keywordInfo = *it; + Waap::Util::Yajl::Map k_api_kw(y); + k_api_kw.gen_str("type", keywordInfo.getType()); + k_api_kw.gen_str("name", keywordInfo.getName()); + k_api_kw.gen_str("value", keywordInfo.getValue()); + k_api_kw.gen_integer("len", keywordInfo.getValue().length()); + } + } + root.gen_key("x_kvs"); + { + Waap::Util::Yajl::Map x_kvs(y); + for (std::vector >::iterator it = kvPairs.begin(); + it != kvPairs.end(); + ++it) { + std::string& k = it->first; + std::string& v = it->second; + x_kvs.gen_str(k, v); + } + } + + root.gen_str("x_body", t.getRequestBody()); + if (!notes.empty()) { + root.gen_key("notes"); + Waap::Util::Yajl::Array jsNotes(y); + for (std::vector::const_iterator it = notes.begin(); it != notes.end(); ++it) { + jsNotes.gen_str(*it); + } + } + + root.gen_bool("send_response", bSendResponse); + root.gen_bool("login_url", false); + } + + return (bSendResponse ? "1" : "0") + y.get_json_str(); + } +} diff --git a/components/security_apps/waap/waap_clib/WaapResultJson.h b/components/security_apps/waap/waap_clib/WaapResultJson.h new file mode 100644 index 0000000..1eb5be1 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapResultJson.h @@ -0,0 +1,21 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include "Waf2Engine.h" +#include "WaapAssetState.h" +#include + +std::string buildWaapResultJson(Waf2ScanResult *m_scanResult, const Waf2Transaction &t, bool bSendResponse, + const std::string &normalizedUri, const std::string &uri, bool bForceBlock, + bool bForceException); diff --git a/components/security_apps/waap/waap_clib/WaapSampleValue.cc b/components/security_apps/waap/waap_clib/WaapSampleValue.cc new file mode 100644 index 0000000..c776800 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapSampleValue.cc @@ -0,0 +1,41 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapSampleValue.h" + +SampleValue::SampleValue(const std::string &sample, + const std::shared_ptr ®exPreconditions) + : + m_sample(sample), + m_regexPreconditions(regexPreconditions), + m_pmWordSet() +{ + if (m_regexPreconditions) { + // Run aho-corasick and related rules once the sample value is known. + // The result pmWordSet is reused later for multiple calls to findMatches on the same sample. + regexPreconditions->pmScan( + Buffer(m_sample.data(), m_sample.size(), Buffer::MemoryType::STATIC), m_pmWordSet); + } +} + +const std::string & +SampleValue::getSampleString() const +{ + return m_sample; +} + +void +SampleValue::findMatches(const Regex &pattern, std::vector &matches) const +{ + pattern.findAllMatches(m_sample, matches, m_regexPreconditions ? &m_pmWordSet : nullptr); +} diff --git a/components/security_apps/waap/waap_clib/WaapSampleValue.h b/components/security_apps/waap/waap_clib/WaapSampleValue.h new file mode 100644 index 0000000..022f533 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapSampleValue.h @@ -0,0 +1,37 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __WAAP_SAMPLE_VALUE_H__ +#define __WAAP_SAMPLE_VALUE_H__ + +#include +#include +#include +#include "Waf2Regex.h" +#include "WaapRegexPreconditions.h" +#include "buffer.h" + +class SampleValue +{ +public: + SampleValue(const std::string &sample, const std::shared_ptr ®exPreconditions); + const std::string &getSampleString() const; + void findMatches(const Regex &pattern, std::vector &matches) const; + +private: + std::string m_sample; + const std::shared_ptr m_regexPreconditions; + Waap::RegexPreconditions::PmWordSet m_pmWordSet; +}; + +#endif // __WAAP_SAMPLE_VALUE_H__ diff --git a/components/security_apps/waap/waap_clib/WaapScanner.cc b/components/security_apps/waap/waap_clib/WaapScanner.cc new file mode 100755 index 0000000..275cf26 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapScanner.cc @@ -0,0 +1,307 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapScanner.h" +#include "WaapScores.h" +#include "i_transaction.h" +#include +#include "debug.h" +#include "reputation_features_events.h" + +USE_DEBUG_FLAG(D_WAAP_SCANNER); + +double Waap::Scanner::getScoreData(Waf2ScanResult& res, const std::string &poolName) +{ + std::string source = m_transaction->getSourceIdentifier(); + + // Extract set of keyword_matches from keyword_matches, then from ngtags + Waap::Keywords::KeywordsSet keywordsSet; + Waap::Keywords::computeKeywordsSet(keywordsSet, res.keyword_matches, res.found_patterns); + + std::string param_name = IndicatorsFiltersManager::generateKey(res.location, res.param_name, m_transaction); + dbgTrace(D_WAAP_SCANNER) << "filter processing for parameter: " << param_name; + m_transaction->getAssetState()->logIndicatorsInFilters(param_name, keywordsSet, m_transaction); + m_transaction->getAssetState()->filterKeywords(param_name, keywordsSet, res.filtered_keywords); + if (m_transaction->getSiteConfig() != nullptr) + { + auto waapParams = m_transaction->getSiteConfig()->get_WaapParametersPolicy(); + if (waapParams != nullptr && waapParams->getParamVal("filtersVerbose", "false") == "true") { + m_transaction->getAssetState()->filterVerbose(param_name, res.filtered_keywords); + } + } + m_transaction->getAssetState()->filterKeywordsByParameters(res.param_name, keywordsSet); + + // The keywords are only removed in production, they are still used while building scores + if (!m_transaction->get_ignoreScore()) { + m_transaction->getAssetState()->removeKeywords(keywordsSet); + } + + // Filter keywords due to wbxml data format + DeepParser &dp = m_transaction->getDeepParser(); + bool isBrokenWBXML = (m_transaction->getContentType() == Waap::Util::CONTENT_TYPE_WBXML) && (dp.depth() == 0) && + (dp.m_key.first().size() == 4 && dp.m_key.first() == "body" && !dp.isWBXmlData()); + + // If wbxml data detected heuristically, or if not detected but declared by content-type in header + if (dp.isWBXmlData() || isBrokenWBXML) { + dbgTrace(D_WAAP_SCANNER) << "Filtering out wbxml keywords. isWbXmlData: " << dp.isWBXmlData() << + ", isBrokenWBXml:" << isBrokenWBXML; + m_transaction->getAssetState()->removeWBXMLKeywords(keywordsSet, res.filtered_keywords); + } + + // update keywords_matches + res.keyword_matches.clear(); + for (auto keyword : keywordsSet) { + res.keyword_matches.push_back(keyword); + } + std::sort(res.keyword_matches.begin(), res.keyword_matches.end()); + + std::string keywords_string; + for (auto pKeyword = keywordsSet.begin(); pKeyword != keywordsSet.end(); ++pKeyword) { + // Add spaces between the items, but not before the first one + if (pKeyword != keywordsSet.begin()) { + keywords_string += " "; + } + + std::string k = *pKeyword; + stripSpaces(k); + keywords_string += k; + } + + std::vector newKeywords; + for (auto pKeyword = keywordsSet.begin(); pKeyword != keywordsSet.end(); ++pKeyword) { + std::string k = *pKeyword; + stripSpaces(k); + // if keyword_string.count(key) < 2: new_keywords.append(key) + if (countSubstrings(keywords_string, k) < 2) { + newKeywords.push_back(k); + } + } + + std::sort(newKeywords.begin(), newKeywords.end()); + + res.scoreArray.clear(); + res.keywordCombinations.clear(); + + if (!newKeywords.empty()) { + // Collect scores of individual keywords + Waap::Scores::calcIndividualKeywords(m_transaction->getAssetState()->scoreBuilder, poolName, newKeywords, + res.scoreArray); + // Collect keyword combinations and their scores. Append scores to scoresArray, + // and also populate m_scanResultKeywordCombinations list + Waap::Scores::calcCombinations(m_transaction->getAssetState()->scoreBuilder, poolName, newKeywords, + res.scoreArray, res.keywordCombinations); + } + + return Waap::Scores::calcArrayScore(res.scoreArray); +} + +bool Waap::Scanner::suspiciousHit(Waf2ScanResult& res, const std::string& location, const std::string& param_name) { + dbgTrace(D_WAAP_SCANNER) << "suspiciousHit processing for parameter: " << param_name << " at " << location << + " num of keywords " << res.keyword_matches.size(); + + res.location = location; + res.param_name = param_name; // remember the param name (analyzer needs it for reporting) + + // Select scores pool by location + std::string poolName = Waap::Scores::getScorePoolNameByLocation(location); + + double score = getScoreData(res, poolName); + + dbgTrace(D_WAAP_SCANNER) << "score: " << score; + // Add record about scores to the notes[] log (also reported in logs) + if (score > 1.0f) { + DetectionEvent(location, res.keyword_matches).notify(); + char buf[128]; + sprintf(buf, "%.3f", score); + const std::string& res_location = m_transaction->getDeepParser().m_key.first(); + const std::string& res_param_name = m_transaction->getDeepParser().m_key.str(); + m_transaction->addNote( + "sc:" + res_location + (res_param_name.empty() ? "" : "/" + res_param_name) + ":" + std::string(buf) + ); + } + + if (m_transaction->shouldIgnoreOverride(res)) { + dbgTrace(D_WAAP_SCANNER) << "Ignoring parameter key/value " << res.param_name << + " due to ignore action in override"; + m_bIgnoreOverride = true; + return false; + } + + res.score = score; + return m_transaction->reportScanResult(res); +} + +int Waap::Scanner::onKv(const char* k, size_t k_len, const char* v, size_t v_len, int flags) { + Waf2ScanResult& res = m_lastScanResult; + DeepParser &dp = m_transaction->getDeepParser(); + std::string key = std::string(k, k_len); + std::string value = std::string(v, v_len); + res.clear(); + dbgTrace(D_WAAP_SCANNER) << "Waap::Scanner::onKv: k='" << key << + "' v='" << value << "'"; + bool isCookiePayload = dp.m_key.first().size() == 6 && dp.m_key.first() == "cookie"; + bool isUrlParamPayload = dp.m_key.first().size() == 9 && dp.m_key.first() == "url_param"; + bool isSplitUrl = dp.m_key.first().size() == 3 && + dp.m_key.first() == "url" && + dp.m_key.str() != ""; + bool isHeaderPayload = dp.m_key.first().size() == 6 && dp.m_key.first() == "header"; + bool isRefererParamPayload = + (dp.m_key.first().size() == 13 && dp.m_key.first() == "referer_param"); + bool isBodyPayload = dp.m_key.first().size() == 4 && dp.m_key.first() == "body"; + dbgTrace(D_WAAP_SCANNER) << "Waap::Scanner::onKv: depth=" << + dp.depth() << "; first='" << dp.m_key.first().c_str() << "'; key='" << + dp.m_key.str().c_str() << "'"; + + // Collect URLs from values for openRedirect feature. + m_transaction->getOpenRedirectState().collect(v, v_len, m_transaction->getHost()); + + // Do not scan our own anti-bot cookie (match by name), it often false alarms. + const std::string& fullKeyStr = dp.m_key.str(); + dbgTrace(D_WAAP_SCANNER) << "Waap::Scanner::onKv: fullKeyStr: '" << fullKeyStr << "'"; + //Get Anti bot cookie + if(isCookiePayload && fullKeyStr == "__fn1522082288") { + m_antibotCookie = value; + dbgTrace(D_WAAP_SCANNER) << "Waap::Scanner::onKv: found Antibot Cookie: '" << m_antibotCookie << "'"; + } + + // Do not scan our own anti-bot cookie (match by name), it often false alarms. + if (isCookiePayload && + (fullKeyStr.find("fnserr") != std::string::npos || + fullKeyStr.find("__fn1522082288") != std::string::npos || + fullKeyStr.find("_fn_nsess") != std::string::npos)) { + dbgTrace(D_WAAP_SCANNER) << "Waap::Scanner::onKv: skip scanning our own anti-bot cookie, by name"; + return 0; + } + // scan for csrf token. + if (isCookiePayload && fullKeyStr == "x-chkp-csrf-token") { + m_transaction->getCsrfState().set_CsrfToken(v, v_len); + } + if (isHeaderPayload && fullKeyStr == "x-chkp-csrf-token") { + m_transaction->getCsrfState().set_CsrfHeaderToken(v, v_len); + } + if (isBodyPayload && fullKeyStr == "x-chkp-csrf-token") { + m_transaction->getCsrfState().set_CsrfFormToken(v, v_len); + } + + if (dp.depth() == 0 && + isCookiePayload && + (v_len >= 2) && + ((v[0] == '"' && v[v_len - 1] == '"') || (v[0] == '\'' && v[v_len - 1] == '\'')) + ) { + dbgTrace(D_WAAP_SCANNER) << "Waap::Scanner::onKv: removing quotes around cookie value: '" << + value << "'"; + // remove the quotes around the value + v++; + v_len -= 2; + value = std::string(v, v_len); + } + res.location = dp.m_key.first(); + res.param_name = dp.m_key.str(); + res.unescaped_line = unescape(value); + + m_transaction->getAssetState()->logParamHit(res, m_transaction); + + std::set paramTypes = m_transaction->getAssetState()->m_filtersMngr->getParameterTypes( + IndicatorsFiltersManager::generateKey(res.location, res.param_name, m_transaction)); + + if (paramTypes.size() == 1 && paramTypes.find("local_file_path") != paramTypes.end()) + { + dbgTrace(D_WAAP_SCANNER) << "found parameter as local path, val : " << value; + if ((value.find("http://") == 0 || value.find("https://") == 0) && !m_transaction->shouldIgnoreOverride(res)) + { + res.score = 10.0; + res.unescaped_line = value; + res.keyword_matches.push_back("url_instead_of_file"); + m_transaction->addNote("sv: found url in " + res.location + "#" + res.param_name); + m_transaction->reportScanResult(res); + return 0; + } + } + // Special value only matched when XML atribute is found. + if (v_len == 36) { + if (value == "08a80340-06d3-11ea-9f87-0242ac11000f" && !m_transaction->shouldIgnoreOverride(res)) { + // Always return max score when addNote("sv: found xml_entity in " + res.location + "#" + res.param_name); + m_transaction->reportScanResult(res); + return 0; + } + } + + // Scan parameter name + bool badUrlEncoding = + dp.m_key.depth() == 2 && + isUrlParamPayload && key != unescape(key) && + (!checkUrlEncoded(k, k_len) || !checkUrlEncoded(v, v_len)); + bool scanNameDueToSplitUrl = dp.m_key.depth() == 2 && isSplitUrl && key != "url.id"; + bool suspiciousName = dp.depth() == 0 && + (isCookiePayload || isRefererParamPayload || isUrlParamPayload || isBodyPayload) && + (!m_transaction->getAssetState()->getSignatures()-> good_header_name_re.hasMatch(key)); + + dbgTrace(D_WAAP_SCANNER) + << "badUrlEncoding=" + << badUrlEncoding + << ", scanNameDueToSplitUrl=" + << scanNameDueToSplitUrl + << ", suspiciousName" + << suspiciousName; + + if (badUrlEncoding || scanNameDueToSplitUrl || suspiciousName) { + dbgTrace(D_WAAP_SCANNER) << "Waap::Scanner::onKv: candidate to scan parameter names"; + + // Deep-scan parameter names + if (m_transaction->getAssetState()->apply(key, res, dp.m_key.first())) { + if (suspiciousHit(res, dp.m_key.first(), dp.m_key.str())) { + // Scanner found enough evidence to report this res + dbgTrace(D_WAAP_SCANNER) << "Waap::Scanner::onKv: SUSPICIOUS PARAM NAME: k='" << + key << "' v='" << value << "'"; +#ifdef ENABLE_WAAP_ATTACK_IN_PARAM + res.param_name = ATTACK_IN_PARAM; + if (m_transaction->getScanResultPtr()) { + m_transaction->getScanResultPtr()->m_isAttackInParam = true; + m_transaction->getScanResultPtr()->param_name = ATTACK_IN_PARAM; + } + else { + dbgWarning(D_WAAP_SCANNER) << "Uninitialized m_scanResult during scanning parameter name (!!!)"; + } +#endif + m_transaction->addNote("sn:" + res.location + (res.param_name.empty() ? "" : "/" + res.param_name)); + } + } + } + Waf2ScanResult param_name_res = res; + res.clear(); + + // Scan parameter value + if (m_transaction->getAssetState()->apply(value, res, dp.m_key.first(), dp.isBinaryData(), + dp.getSplitType())) + { + if (!param_name_res.keyword_matches.empty() && !res.keyword_matches.empty() && + param_name_res.location == "url_param") + { + dbgTrace(D_WAAP_SCANNER) << "Found suspicios content in param name and value. Merging scans"; + res.mergeFrom(param_name_res); + } + + if (suspiciousHit(res, dp.m_key.first(), dp.m_key.str())) { + // Scanner found enough evidence to report this res + dbgTrace(D_WAAP_SCANNER) << "Waap::Scanner::onKv: SUSPICIOUS VALUE: k='" << key << + "' v='" << value << "'"; + m_transaction->addNote("sv:" + res.location + (res.param_name.empty() ? "" : "/" + res.param_name)); + } + } + + return 0; +} diff --git a/components/security_apps/waap/waap_clib/WaapScanner.h b/components/security_apps/waap/waap_clib/WaapScanner.h new file mode 100644 index 0000000..610473a --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapScanner.h @@ -0,0 +1,52 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __WAAP_SCANNER_H__ +#define __WAAP_SCANNER_H__ + +#include "ParserBase.h" +#include "ScanResult.h" +#include "i_transaction.h" +#include "WaapAssetState.h" +#include + +namespace Waap { + class Scanner : public IParserReceiver + { + public: + Scanner(IWaf2Transaction *transaction) + : + m_lastScanResult(), + m_transaction(transaction), + m_antibotCookie(), + m_bIgnoreOverride(false) + { + } + bool suspiciousHit(Waf2ScanResult &res, const std::string &location, const std::string ¶m_name); + int onKv(const char* k, size_t k_len, const char* v, size_t v_len, int flags) override; + + const std::string &getAntibotCookie() const { return m_antibotCookie; } + bool getIgnoreOverride() { return m_bIgnoreOverride; }; + const Waf2ScanResult &getLastScanResult() const { return m_lastScanResult; } + private: + double getScoreData(Waf2ScanResult& res, const std::string &poolName); + bool shouldIgnoreOverride(const Waf2ScanResult &res); + + Waf2ScanResult m_lastScanResult; + IWaf2Transaction *m_transaction; + std::string m_antibotCookie; + bool m_bIgnoreOverride; + }; +} + +#endif // __WAAP_SCANNER_H__ diff --git a/components/security_apps/waap/waap_clib/WaapScores.cc b/components/security_apps/waap/waap_clib/WaapScores.cc new file mode 100644 index 0000000..51b7a62 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapScores.cc @@ -0,0 +1,113 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapScores.h" +#include +#include +#include "ScoreBuilder.h" +#include "WaapDefines.h" +#include "debug.h" + +USE_DEBUG_FLAG(D_WAAP_SCORE_BUILDER); + +namespace Waap { +namespace Scores { + +std::string getScorePoolNameByLocation(const std::string &location) { + std::string poolName = KEYWORDS_SCORE_POOL_BASE; + if (location == "header") { + poolName = KEYWORDS_SCORE_POOL_HEADERS; + } + return poolName; +} + +void +addKeywordScore( + const ScoreBuilder& scoreBuilder, + const std::string &poolName, + std::string keyword, + double defaultScore, + std::vector& scoresArray) +{ + scoresArray.push_back(scoreBuilder.getSnapshotKeywordScore(keyword, defaultScore, poolName)); +} + +// Calculate score of individual keywords +void +calcIndividualKeywords( + const ScoreBuilder& scoreBuilder, + const std::string &poolName, + const std::vector& keyword_matches, + std::vector& scoresArray) +{ + std::vector keywords = keyword_matches; // deep copy!! (PERFORMANCE WARNING!) + std::sort(keywords.begin(), keywords.end()); + + for (auto pKeyword = keywords.begin(); pKeyword != keywords.end(); ++pKeyword) { + addKeywordScore(scoreBuilder, poolName, *pKeyword, 2.0f, scoresArray); + } +} + +// Calculate keyword combinations and their scores +void +calcCombinations( + const ScoreBuilder& scoreBuilder, + const std::string &poolName, + const std::vector& keyword_matches, + std::vector& scoresArray, + std::vector& keyword_combinations) +{ + keyword_combinations.clear(); + + for (size_t i = 0; i < keyword_matches.size(); ++i) { + std::vector combinations; + for (size_t j = i; j < std::min(i + 2, keyword_matches.size()); ++j) { + combinations.push_back(keyword_matches[j]); + } + if (combinations.size() > 1) { + // Must be sorted to build a string that exactly matches the keys (strings) + // from signature_scores database. + std::sort(combinations.begin(), combinations.end()); + std::string combination; + // note that std::set<> container output sorted data when iterated. + for (auto it = combinations.begin(); it != combinations.end(); it++) { + // add space between all items, except the first one + if (it != combinations.begin()) { + combination += " "; + } + combination += *it; + } + addKeywordScore(scoreBuilder, poolName, combination, 1.0f, scoresArray); + keyword_combinations.push_back(combination); + } + } +} + +double +calcArrayScore(std::vector& scoreArray) +{ + // Calculate cumulative score from array of individual scores + double score = 1.0f; + for (auto pScore = scoreArray.begin(); pScore != scoreArray.end(); ++pScore) { + dbgTrace(D_WAAP_SCORE_BUILDER) << "scoreArr[]=" << *pScore; + double left = 10.0f - score; + double divisor = (*pScore / 3.0f + 10.0f); // note: divisor can't be empty because + // *pScore is always positive and there's a +10 offset + score = 10.0f - left * 10.0f / divisor; + } + dbgTrace(D_WAAP_SCORE_BUILDER) << "calculated score: " << score; + return score; +} + +} +} diff --git a/components/security_apps/waap/waap_clib/WaapScores.h b/components/security_apps/waap/waap_clib/WaapScores.h new file mode 100644 index 0000000..71dcc81 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapScores.h @@ -0,0 +1,53 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include "ScoreBuilder.h" + +namespace Waap { +namespace Scores { + +std::string getScorePoolNameByLocation(const std::string &location); + +void +addKeywordScore( + const ScoreBuilder& scoreBuilder, + const std::string &poolName, + std::string keyword, + double defaultScore, + std::vector& scoresArray); + +// Calculate score of individual keywords +void +calcIndividualKeywords( + const ScoreBuilder& scoreBuilder, + const std::string &poolName, + const std::vector& keyword_matches, + std::vector& scoresArray); + +// Calculate keyword combinations and their scores +void +calcCombinations( + const ScoreBuilder& scoreBuilder, + const std::string &poolName, + const std::vector& keyword_matches, + std::vector& scoresArray, + std::vector& keyword_combinations); + +double calcArrayScore(std::vector& scoreArray); + +} +} diff --git a/components/security_apps/waap/waap_clib/WaapTrigger.cc b/components/security_apps/waap/waap_clib/WaapTrigger.cc new file mode 100755 index 0000000..131fee9 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapTrigger.cc @@ -0,0 +1,79 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapTrigger.h" +#include "Waf2Util.h" + +namespace Waap { +namespace Trigger { + +Log::Log() +: + verbosity("standard"), + complianceWarnings(true), + complianceViolations(true), + acAllow(true), + acDrop(true), + tpDetect(true), + tpPrevent(true), + webRequests(true), + webUrlPath(true), + webUrlQuery(true), + webHeaders(false), + webBody(true), + logToCloud(true), + logToAgent(true), + extendLogging(false), + responseCode(false), + responseBody(false), + extendLoggingMinSeverity("") +{ +} + +bool +Log::operator==(const Log &other) const +{ + return (verbosity == other.verbosity) && + (complianceWarnings == other.complianceWarnings) && + (complianceViolations == other.complianceViolations) && + (acAllow == other.acAllow) && + (acDrop == other.acDrop) && + (tpDetect == other.tpDetect) && + (tpPrevent == other.tpPrevent) && + (webRequests == other.webRequests) && + (webUrlPath == other.webUrlPath) && + (webHeaders == other.webHeaders) && + (webUrlQuery == other.webUrlQuery) && + (webBody == other.webBody) && + (logToCloud == other.logToCloud) && + (logToAgent == other.logToAgent); +} + +Trigger::Trigger():triggerType("log"), log(std::make_shared()) +{ +} + +bool +Trigger::operator==(const Trigger &other) const +{ + return (triggerType == other.triggerType) && + (Waap::Util::compareObjects(log, other.log)); +} + +bool Policy::operator==(const Policy &other) const +{ + return triggers == other.triggers; +} + +} +} diff --git a/components/security_apps/waap/waap_clib/WaapTrigger.h b/components/security_apps/waap/waap_clib/WaapTrigger.h new file mode 100755 index 0000000..e1493fc --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapTrigger.h @@ -0,0 +1,156 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include +#include +#include +#include +#include +#include +#include "debug.h" + +USE_DEBUG_FLAG(D_WAAP); + +namespace Waap { +namespace Trigger { +using boost::algorithm::to_lower_copy; + +struct Log { + template + void serialize(_A &ar) { + ar(cereal::make_nvp("verbosity", verbosity)); + verbosity = to_lower_copy(verbosity); + ar(cereal::make_nvp("complianceWarnings", complianceWarnings)); + ar(cereal::make_nvp("complianceViolations", complianceViolations)); + ar(cereal::make_nvp("acAllow", acAllow)); + ar(cereal::make_nvp("acDrop", acDrop)); + ar(cereal::make_nvp("tpDetect", tpDetect)); + ar(cereal::make_nvp("tpPrevent", tpPrevent)); + ar(cereal::make_nvp("webRequests", webRequests)); + ar(cereal::make_nvp("webUrlPath", webUrlPath)); + ar(cereal::make_nvp("webUrlQuery", webUrlQuery)); + ar(cereal::make_nvp("webBody", webBody)); + ar(cereal::make_nvp("logToCloud", logToCloud)); + ar(cereal::make_nvp("logToAgent", logToAgent)); + + try + { + ar(cereal::make_nvp("webHeaders", webHeaders)); + } + catch (const cereal::Exception &e) + { + ar.setNextName(nullptr); + dbgDebug(D_WAAP) << "failed to load webHeaders field. Error: " << e.what(); + } + + try + { + ar(cereal::make_nvp("extendLogging", extendLogging)); + } + catch(const cereal::Exception &e) + { + ar.setNextName(nullptr); + dbgDebug(D_WAAP) << "Failed to load extendedLogging field. Error: " << e.what(); + } + + + if (extendLogging) + { + try + { + ar(cereal::make_nvp("extendLoggingMinSeverity", extendLoggingMinSeverity)); + } + catch(const cereal::Exception &e) + { + ar.setNextName(nullptr); + dbgDebug(D_WAAP) << "Failed to load extendLoggingMinSeverity field. Error: " << e.what(); + } + + try + { + ar(cereal::make_nvp("responseCode", responseCode)); + } + catch(const cereal::Exception &e) + { + ar.setNextName(nullptr); + dbgDebug(D_WAAP) << "Failed to load responseCode field. Error: " << e.what(); + } + + try + { + ar(cereal::make_nvp("responseBody", responseBody)); + } + catch(const cereal::Exception &e) + { + ar.setNextName(nullptr); + dbgDebug(D_WAAP) << "Failed to load responseBody field. Error: " << e.what(); + } + } + } + + Log(); + bool operator==(const Log &other) const; + + std::string verbosity; + bool complianceWarnings; + bool complianceViolations; + bool acAllow; + bool acDrop; + bool tpDetect; + bool tpPrevent; + bool webRequests; + bool webUrlPath; + bool webUrlQuery; + bool webHeaders; + bool webBody; + bool logToCloud; + bool logToAgent; + bool extendLogging; + bool responseCode; + bool responseBody; + std::string extendLoggingMinSeverity; +}; + +struct Trigger { + template + void serialize(_A &ar) { + ar(cereal::make_nvp("$triggerType", triggerType)); + triggerType = to_lower_copy(triggerType); + + // Currently, only load triggers of type "log". + if (triggerType == "log") { + ar(cereal::make_nvp("log", *log)); + } + } + + Trigger(); + bool operator==(const Trigger &other) const; + + std::string triggerType; + std::shared_ptr log; +}; + +struct Policy { + template + Policy(_A &ar) { + ar(cereal::make_nvp("triggers", triggers)); + } + + bool operator==(const Policy &other) const; + + std::vector triggers; +}; + +} +} diff --git a/components/security_apps/waap/waap_clib/WaapValueStatsAnalyzer.cc b/components/security_apps/waap/waap_clib/WaapValueStatsAnalyzer.cc new file mode 100755 index 0000000..b1bb373 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapValueStatsAnalyzer.cc @@ -0,0 +1,264 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "WaapValueStatsAnalyzer.h" +#include +#include +#include "debug.h" +#include "Waf2Util.h" + +USE_DEBUG_FLAG(D_WAAP); + +bool checkUrlEncoded(const char *buf, size_t len) +{ + dbgFlow(D_WAAP); + size_t i = 0; + int hex_characters_to_follow = 0; + + for (; i < len; i++) { + char ch = buf[i]; + if (ch == '%' && hex_characters_to_follow == 2) { + continue; + } + + if (hex_characters_to_follow > 0) { + hex_characters_to_follow--; + if (isHexDigit(ch)) { + continue; + } + return false; + } else if (ch == '%') { + hex_characters_to_follow = 2; + continue; + } + + if (Waap::Util::isAlphaAsciiFast(static_cast(ch)) || isdigit(ch)) { + continue; + } + + switch (ch) { + case '.': + case '-': + case '_': + case '~': + case '!': + case '*': + case '\'': + case '(': + case ')': + case ';': + case ':': + case '@': + case '&': + case '=': + case '+': + case '$': + case ',': + case '/': + case '?': + case '#': + case '[': + case ']': + continue; + default: + return false; + } + } + + return true; +} + +ValueStatsAnalyzer::ValueStatsAnalyzer(const std::string &cur_val) + : + hasCharSlash(false), + hasCharColon(false), + hasCharAmpersand(false), + hasCharEqual(false), + hasTwoCharsEqual(false), + hasCharSemicolon(false), + hasCharPipe(false), + longestZerosSeq{0}, + isUTF16(false), + canSplitSemicolon(true), + canSplitPipe(true), + hasSpace(false), + isUrlEncoded(false) +{ + unsigned int zerosSeq[2] = {0}; + bool lastNul = false; // whether last processed character was ASCII NUL + size_t curValLength = cur_val.length(); + + if (curValLength == 0) { + canSplitSemicolon = false; + canSplitPipe = false; + return; + } + + // Decide the input is candidate for UTF16 if all the following rules apply: + // 1. Input buffer length is longer than 2 bytes + // 2. Input buffer length is divisible by 2 + isUTF16 = (curValLength > 2) && (curValLength % 2 == 0); + + for (size_t i = 0; i < curValLength; ++i) + { + unsigned char ch = (unsigned char)cur_val[i]; + + switch(ch) { + case '/': + hasCharSlash = true; + break; + case ':': + hasCharColon = true; + break; + case '&': + hasCharAmpersand = true; + break; + case '=': + if (!hasTwoCharsEqual) { + if (hasCharEqual) { + hasTwoCharsEqual = true; + } + hasCharEqual = true; + } + break; + case ';': + hasCharSemicolon = true; + break; + case '|': + hasCharPipe = true; + break; + } + + // The index will be 0 for even, and 1 for odd offsets + int index = i % 2; + + // Compute longest sequence of ASCII NUL bytes over even and odd offsets in cur_val + if (ch == 0) + { + if (lastNul) + { + // UTF-16 consists of subsequent pairs of bytes. Cancel UTF16 detection if there is a NUL bytes pair. + // (but allow such a pair at the end of the input buffer: UTF16 could be "NUL terminated" this way) + if (isUTF16 && (index == 1) && (i + 1 < curValLength)) { + isUTF16 = false; + } + + // Anytime two ASCII NULs are encountered in a row - terminate counting the NUL-sequence length. + zerosSeq[0] = 0; + zerosSeq[1] = 0; + } + else + { + zerosSeq[index]++; + longestZerosSeq[index] = std::max(zerosSeq[index], longestZerosSeq[index]); + } + + lastNul = true; + } + else + { + zerosSeq[index] = 0; + lastNul = false; + } + + bool isAlphaNumeric = Waap::Util::isAlphaAsciiFast(ch) || isdigit(ch); + + if (canSplitSemicolon && !isAlphaNumeric) { + switch (ch) { + case '.': + case '-': + case '_': + case '=': + case ',': + case '(': + case ')': + case ';': + break; + default: + // Only alphanumeric characters and characters listed above are allowed, anything else disables + canSplitSemicolon = false; + } + } + + if (canSplitPipe && !isAlphaNumeric) { + switch (ch) { + case ':': + case '?': + case '.': + case '-': + case '_': + case '=': + case ',': + case '[': + case ']': + case '/' : + case ' ': + case '\f': + case '\v': + case '\t': + case '\r': + case '\n': + case '(': + case ')': + case '|': + break; + default: + // Only alphanumeric characters and characters listed above are allowed, anything else disables + canSplitPipe = false; + } + } + } + + // Only decode UTF16 if at least one longest zero bytes sequence (computed over odd + // or over even input bytes) is longer than 2. + // If both sequences are too short - do not decode UTF16 on such input. + if (longestZerosSeq[0] <= 2 && longestZerosSeq[1] <= 2) { + isUTF16 = false; + } + + // Detect URLEncode value + size_t ofs = 0; + for (size_t i = 0 ; i < cur_val.size(); ++i) { + char ch = cur_val[i]; + + if (isspace(ch)) { + hasSpace = true; + isUrlEncoded = false; + break; + } + + if (ofs == 0) { + if (ch == '%') { + ofs++; + } + } + else if (ofs <= 2) { + if (!isHexDigit(ch)) { + isUrlEncoded = false; + break; // at least one broken URLEncode sequence detected + } + if (ofs == 2) { + isUrlEncoded = true; // complete '%hh' sequence + ofs = 0; // search for next '%' character + } + else { + ofs++; + } + } + } + + // Cancel url decoding if partial match after '%' is found, or if potential specific utf8 evasion is suspected + if (ofs != 0) { + isUrlEncoded = false; + } +} diff --git a/components/security_apps/waap/waap_clib/WaapValueStatsAnalyzer.h b/components/security_apps/waap/waap_clib/WaapValueStatsAnalyzer.h new file mode 100755 index 0000000..fcd2e30 --- /dev/null +++ b/components/security_apps/waap/waap_clib/WaapValueStatsAnalyzer.h @@ -0,0 +1,39 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once +#include + +bool checkUrlEncoded(const char *buf, size_t len); + +// Process value (buffer) and calculate some statistics/insights over it, for use in later processing. +// The insights are computed in te same for loop for performance reasons. +struct ValueStatsAnalyzer +{ + ValueStatsAnalyzer(const std::string &cur_val); + bool hasCharSlash; + bool hasCharColon; + bool hasCharAmpersand; + bool hasCharEqual; + bool hasTwoCharsEqual; + bool hasCharSemicolon; + bool hasCharPipe; + unsigned int longestZerosSeq[2]; // longest zeros sequence. counted over even (index 0) and odd (index 1) offsets + bool isUTF16; + bool canSplitSemicolon; + bool canSplitPipe; + bool hasSpace; + bool isUrlEncoded; +}; + + diff --git a/components/security_apps/waap/waap_clib/Waf2Engine.cc b/components/security_apps/waap/waap_clib/Waf2Engine.cc new file mode 100755 index 0000000..c86432f --- /dev/null +++ b/components/security_apps/waap/waap_clib/Waf2Engine.cc @@ -0,0 +1,2271 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "Waf2Engine.h" +#include "waap.h" +#include "WaapAssetState.h" +#include "CidrMatch.h" +#include "ParserRaw.h" +#include "ParserUrlEncode.h" +#include "ParserMultipartForm.h" +#include "ParserXML.h" +#include "ParserJson.h" +#include "ContentTypeParser.h" +#include "Waf2Util.h" +#include "debug.h" +#include "DeepAnalyzer.h" +#include "WaapConfigApplication.h" +#include "WaapConfigApi.h" +#include "WaapDefines.h" +#include "WaapTrigger.h" +#include "WaapScores.h" +#include "WaapDecision.h" +#include "WaapConversions.h" +#include "WaapResultJson.h" +#include "WaapAssetStatesManager.h" +#include "log_generator.h" +#include "config.h" +#include "WaapOverrideFunctor.h" +#include "WaapOpenRedirect.h" +#include "WaapOpenRedirectPolicy.h" +#include "WaapErrorDisclosurePolicy.h" +#include +#include "generic_rulebase/parameters_config.h" +#include +#include "ParserDelimiter.h" +#include "OpenRedirectDecision.h" +#include "DecisionType.h" +#include "generic_rulebase/triggers_config.h" +#include "config.h" +#include "LogGenWrapper.h" +#include "reputation_features_events.h" +#include "telemetry.h" +#include "agent_core_utilities.h" + +USE_DEBUG_FLAG(D_WAAP); +USE_DEBUG_FLAG(D_WAAP_ULIMITS); +USE_DEBUG_FLAG(D_WAAP_BOT_PROTECTION); +using namespace ReportIS; + +#define MAX_REQUEST_BODY_SIZE (2*1024) +#define MAX_RESPONSE_BODY_SIZE (2*1024) +#define MAX_RESPONSE_BODY_SIZE_ERR_DISCLOSURE (2*1024) +#define OVERRIDE_ACCEPT "Accept" +#define OVERRIDE_DROP "Drop" +#define OVERRIDE_IGNORE "Ignore" + +// Score threshold below which the match won't be considered +#define SCORE_THRESHOLD (1.4f) + +static const ParameterBehavior action_ignore(BehaviorKey::ACTION, BehaviorValue::IGNORE); + +void Waf2Transaction::learnScore(ScoreBuilderData& data, const std::string &poolName) +{ + m_pWaapAssetState->scoreBuilder.analyzeFalseTruePositive(data, poolName, !m_ignoreScore); + + if (m_ignoreScore) // check if we are in building scores state + { + // Set the relative reputation to max to ensure learning fp in score builder + data.m_relativeReputation = MAX_RELATIVE_REPUTATION; + } + m_pWaapAssetState->scoreBuilder.checkBadSourcesForLearning( + data.m_relativeReputation, + data.m_sourceIdentifier, + data.m_userAgent); +} + +void Waf2Transaction::start_response(int response_status, int http_version) +{ + dbgTrace(D_WAAP) << "[transaction:" << this << "] start_response(response_status=" << response_status + << "," << " http_version=" << http_version << ")"; + m_responseStatus = response_status; + + if(m_responseStatus == 404) + { + // Create error limiting policy (lazy, on first request) + if(m_siteConfig != NULL) { + const std::shared_ptr errorLimitingPolicy = + m_siteConfig->get_ErrorLimitingPolicy(); + + if (errorLimitingPolicy && errorLimitingPolicy->getRateLimitingEnforcementStatus()) { + if (m_pWaapAssetState->getErrorLimitingState() == nullptr) { + m_pWaapAssetState->createErrorLimitingState(errorLimitingPolicy); + dbgTrace(D_WAAP) << "Waf2Transaction::start_response: Create Error Limiting State"; + } + + bool errorLimitingLog = false; + bool blockDueToErrorLimiting = Waap::ErrorLimiting::enforce + (m_source_identifier, m_uriPath, m_pWaapAssetState, errorLimitingLog); + + dbgTrace(D_WAAP) << + "Waf2Transaction::start_response: response code: 404 :: Error Limiting Block : " << + blockDueToErrorLimiting; + + auto decision = m_waapDecision.getDecision(ERROR_LIMITING_DECISION); + decision->setLog(errorLimitingLog); + decision->setBlock(blockDueToErrorLimiting); + } + } + } +} + +void Waf2Transaction::start_response_hdrs() +{ + dbgTrace(D_WAAP) << "[transaction:" << this << "] start_response_hdrs"; +} + +void Waf2Transaction::add_response_hdr(const char* name, int name_len, const char* value, int value_len) +{ + dbgTrace(D_WAAP) << "[transaction:" << this << "] add_response_hdr(name='" << std::string(name, name_len) << + "', value='" << std::string(value, value_len) << "')"; + + // Detect location header and remember its value + static const char location[] = "location"; + + auto openRedirectPolicy = m_siteConfig ? m_siteConfig->get_OpenRedirectPolicy() : NULL; + if (openRedirectPolicy && openRedirectPolicy->enable && + memcaseinsensitivecmp(name, name_len, location, sizeof(location) - 1)) { + std::string redirectUrl = std::string(value, value_len); + dbgTrace(D_WAAP) << "Detected the redirect 'Location' header: '" << redirectUrl << "'"; + + if (m_responseStatus >= 300 && m_responseStatus < 400 && m_openRedirectState.testRedirect(redirectUrl)) { + dbgTrace(D_WAAP) << "Waf2Transaction::decideResponse: openRedirect detected (enforce=" << + openRedirectPolicy->enforce << ")"; + auto decision = std::dynamic_pointer_cast( + m_waapDecision.getDecision(OPEN_REDIRECT_DECISION)); + decision->setLog(true); + decision->setBlock(openRedirectPolicy->enforce); + decision->setLink(redirectUrl); + } + } + + if (m_responseStatus >= 400 && m_responseStatus <= 599) { + auto errorDisclosurePolicy = m_siteConfig ? m_siteConfig->get_ErrorDisclosurePolicy() : NULL; + if (errorDisclosurePolicy && errorDisclosurePolicy->enable) { + // Scan response header values + Waf2ScanResult res; + if (m_pWaapAssetState->apply(std::string(value, value_len), res, "resp_header")) { + // Found some signatures in response! + delete m_scanResult; + m_scanResult = new Waf2ScanResult(res); + dbgTrace(D_WAAP) << "found indicators in response header"; + auto decision = m_waapDecision.getDecision(ERROR_DISCLOSURE_DECISION); + decision->setLog(true); + decision->setBlock(errorDisclosurePolicy->enforce); + } + } + } +} + +void Waf2Transaction::end_response_hdrs() +{ + dbgTrace(D_WAAP) << "[transaction:" << this << "] end_response_hdrs"; + + // Enable response body processing only if response scanning is enabled in policy + auto errorDisclosurePolicy = m_siteConfig ? m_siteConfig->get_ErrorDisclosurePolicy() : NULL; + m_responseInspectReasons.setErrorDisclosure(errorDisclosurePolicy && errorDisclosurePolicy->enable); + + // OpenRedirect is only interested to see response headers, the body + m_responseInspectReasons.setOpenRedirect(false); +} + +void Waf2Transaction::start_response_body() +{ + dbgTrace(D_WAAP) << "[transaction:" << this << "] start_response_body"; + m_response_body_bytes_received = 0; + m_response_body.clear(); +} + +void Waf2Transaction::add_response_body_chunk(const char* data, int data_len) +{ + dbgTrace(D_WAAP) << "[transaction:" << this << "] add_response_body_chunk (" << data_len << " bytes)"; + m_response_body_bytes_received += data_len; + + auto errorDisclosurePolicy = m_siteConfig ? m_siteConfig->get_ErrorDisclosurePolicy() : NULL; + if (errorDisclosurePolicy && errorDisclosurePolicy->enable && + (m_responseStatus >= 400 && m_responseStatus <= 599)) { + // Collect up to MAX_RESPONSE_BODY_SIZE_ERR_DISCLOSURE of input data for each response + if (m_response_body_err_disclosure.length() + data_len <= MAX_RESPONSE_BODY_SIZE_ERR_DISCLOSURE) { + m_response_body_err_disclosure.append(data, (size_t)data_len); + } + else if (m_response_body_err_disclosure.length() < MAX_RESPONSE_BODY_SIZE_ERR_DISCLOSURE) { + size_t piece = MAX_RESPONSE_BODY_SIZE_ERR_DISCLOSURE - m_response_body_err_disclosure.length(); + // Note: piece is guaranteed to be > data_len, so the write below is safe. + m_response_body_err_disclosure.append(data, piece); + } + else { + m_responseInspectReasons.setErrorDisclosure(false); + } + } + + if (m_response_body_err_disclosure.length() <= MAX_RESPONSE_BODY_SIZE_ERR_DISCLOSURE) { + // Scan now, buffer is filled up. + scanErrDisclosureBuffer(); + } + + // Collect up to MAX_RESPONSE_BODY_SIZE of input data for each response + if (m_response_body.length() + data_len <= MAX_RESPONSE_BODY_SIZE) { + m_response_body.append(data, (size_t)data_len); + } + else if (m_response_body.length() < MAX_RESPONSE_BODY_SIZE) { + size_t piece = MAX_RESPONSE_BODY_SIZE - m_response_body.length(); + // Note: piece is guaranteed to be > data_len, so the write below is safe. + m_response_body.append(data, piece); + } + else { + // No more need to collect response body for log (got enough data - up to MAX_RESPONSE_BODY_SIZE collected) + m_responseInspectReasons.setCollectResponseForLog(false); + } +} + +void Waf2Transaction::end_response_body() +{ + dbgTrace(D_WAAP) << "[transaction:" << this << "] end_response_body"; +} + +void Waf2Transaction::scanErrDisclosureBuffer() +{ + if (m_responseStatus >= 400 && m_responseStatus <= 599) { + auto errorDisclosurePolicy = m_siteConfig ? m_siteConfig->get_ErrorDisclosurePolicy() : NULL; + if (errorDisclosurePolicy && errorDisclosurePolicy->enable) { + // Scan response body chunks. + Waf2ScanResult res; + if (m_pWaapAssetState->apply(std::string(m_response_body_err_disclosure.data(), + m_response_body_err_disclosure.size()), res, "resp_body")) { + // Found some signatures in response! + delete m_scanResult; + m_scanResult = new Waf2ScanResult(res); + dbgTrace(D_WAAP) << "found indicators in response body"; + auto decision = m_waapDecision.getDecision(ERROR_DISCLOSURE_DECISION); + decision->setLog(true); + decision->setBlock(errorDisclosurePolicy->enforce); + } + } + } + m_responseInspectReasons.setErrorDisclosure(false); +} + +void Waf2Transaction::end_response() +{ + dbgTrace(D_WAAP) << "[transaction:" << this << "] end_response"; +} + +void Waf2Transaction::setCurrentAssetState(IWaapConfig* sitePolicy) +{ + I_WaapAssetStatesManager* pWaapAssetStatesManager = + Singleton::Consume::by(); + std::shared_ptr pCurrentWaapAssetState = + pWaapAssetStatesManager->getWaapAssetStateById(sitePolicy->get_AssetId()); + + if (!pCurrentWaapAssetState || pCurrentWaapAssetState->getSignatures()->fail()) + { + dbgWarning(D_WAAP) << "[transaction:" << this << "] " + "couldn't set waapAssetState for asset... using original waapAssetState"; + return; + } + + m_pWaapAssetState = pCurrentWaapAssetState; +} + +void Waf2Transaction::clearRequestParserState() { + if (m_requestBodyParser != NULL) { + delete m_requestBodyParser; + m_requestBodyParser = NULL; + } +} + +Waf2Transaction::Waf2Transaction() : + TableOpaqueSerialize(this), + m_pWaapAssetState(NULL), + m_ignoreScore(false), + m_remote_port(0), + m_local_port(0), + m_csrfState(), + m_userLimitsState(nullptr), + m_siteConfig(NULL), + m_contentType(Waap::Util::CONTENT_TYPE_UNKNOWN), + m_requestBodyParser(NULL), + m_tagHist{0}, + m_tagHistPos(0), + m_isUrlValid(false), + m_scanner(this), + m_deepParser(m_pWaapAssetState, m_scanner, this), + m_deepParserReceiver(m_deepParser), + m_scanResult(NULL), + m_request_body_bytes_received(0), + m_response_body_bytes_received(0), + m_processedUri(false), + m_processedHeaders(false), + m_isScanningRequired(false), + m_responseStatus(0), + m_responseInspectReasons(), + m_responseInjectReasons(), + m_index(-1), + m_triggerLog(), + m_waf2TransactionFlags() +{ + is_hybrid_mode = + Singleton::exists() ? + Singleton::Consume::by()->getOrchestrationMode() == OrchestrationMode::HYBRID + : false; + if (is_hybrid_mode) { + max_grace_logs = getProfileAgentSettingWithDefault( + 10, + "rulebase.initialForcedSecurityLogsToLocalStorage.count" + ); + } +} + +Waf2Transaction::Waf2Transaction(std::shared_ptr pWaapAssetState) : + TableOpaqueSerialize(this), + m_pWaapAssetState(pWaapAssetState), + m_ignoreScore(false), + m_remote_port(0), + m_local_port(0), + m_csrfState(), + m_userLimitsState(nullptr), + m_siteConfig(NULL), + m_contentType(Waap::Util::CONTENT_TYPE_UNKNOWN), + m_requestBodyParser(NULL), + m_tagHist{0}, + m_tagHistPos(0), + m_isUrlValid(false), + m_scanner(this), + m_deepParser(m_pWaapAssetState, m_scanner, this), + m_deepParserReceiver(m_deepParser), + m_scanResult(NULL), + m_request_body_bytes_received(0), + m_response_body_bytes_received(0), + m_processedUri(false), + m_processedHeaders(false), + m_isScanningRequired(false), + m_responseStatus(0), + m_responseInspectReasons(), + m_responseInjectReasons(), + m_index(-1), + m_triggerLog(), + m_waf2TransactionFlags() +{ + is_hybrid_mode = + Singleton::exists() ? + Singleton::Consume::by()->getOrchestrationMode() == OrchestrationMode::HYBRID + : false; + if (is_hybrid_mode) { + max_grace_logs = getProfileAgentSettingWithDefault( + 10, + "rulebase.initialForcedSecurityLogsToLocalStorage.count" + ); + } +} + +Waf2Transaction::~Waf2Transaction() { + dbgTrace(D_WAAP) << "Waf2Transaction::~Waf2Transaction: deleting m_requestBodyParser"; + delete m_requestBodyParser; + dbgTrace(D_WAAP) << "Waf2Transaction::~Waf2Transaction: deleting m_scanResult"; + delete m_scanResult; +} + +HeaderType Waf2Transaction::detectHeaderType(const char* name, int name_len) { + // Detect host header + static const char host[] = "host"; + static const char user_agent[] = "user-agent"; + static const char content_type[] = "content-Type"; + static const char cookie[] = "cookie"; + static const char referer[] = "referer"; + + if (memcaseinsensitivecmp(name, name_len, host, sizeof(host) - 1)) { + return HeaderType::HOST_HEADER; + } + if (memcaseinsensitivecmp(name, name_len, user_agent, sizeof(user_agent) - 1)) { + return HeaderType::USER_AGENT_HEADER; + } + if (memcaseinsensitivecmp(name, name_len, content_type, sizeof(content_type) - 1)) { + return HeaderType::CONTENT_TYPE_HEADER; + } + if (memcaseinsensitivecmp(name, name_len, cookie, sizeof(cookie) - 1)) { + return HeaderType::COOKIE_HEADER; + } + if (memcaseinsensitivecmp(name, name_len, referer, sizeof(referer) - 1)) { + return HeaderType::REFERER_HEADER; + } + return UNKNOWN_HEADER; +} + +HeaderType Waf2Transaction::checkCleanHeader(const char* name, int name_len, const char* value, int value_len) const +{ + if (m_pWaapAssetState != nullptr) { + for (auto it = m_pWaapAssetState->getSignatures()->headers_re.begin(); + it != m_pWaapAssetState->getSignatures()->headers_re.end(); + ++it) { + const std::string& reHeaderName = it->first; + Regex* pRegex = it->second; + if (memcaseinsensitivecmp(name, name_len, reHeaderName.data(), reHeaderName.size())) { + dbgTrace(D_WAAP) << "[transaction:" << this << "] special header '" << std::string(name, name_len) << + "' - scan with regex '" << pRegex->getName().c_str() << "' to determine cleanliness ..."; + if(pRegex->hasMatch(std::string(value, value_len))) { + dbgTrace(D_WAAP) << "[transaction:" << this << "] special header '" << + std::string(name, name_len) << " is clean"; + return CLEAN_HEADER; + } + return OTHER_KNOWN_HEADERS; + } + } + + static const std::string x_newrelic_id("x-newrelic-id"); + static const std::string x_newrelic_transaction("x-newrelic-transaction"); + if (memcaseinsensitivecmp(name, name_len, x_newrelic_id.data(), x_newrelic_id.size()) || + memcaseinsensitivecmp(name, name_len, x_newrelic_transaction.data(), x_newrelic_transaction.size())) { + dbgTrace(D_WAAP) << "[transaction:" << this << "] special header '" << std::string(name, name_len) << + "' - detect base64 to determine cleanliness ..."; + + std::string result; + int decodedCount = 0; + int deletedCount = 0; + + // Detect potential base64 matches + Waap::Util::b64Decode(std::string(value, value_len), b64DecodeChunk, decodedCount, deletedCount, result); + + if (result.empty() && (decodedCount + deletedCount == 1)) { + // Decoded 1 base64 chunk and nothing left behind it + dbgTrace(D_WAAP) << "[transaction:" << this << "] special header '" << + std::string(name, name_len) << " is clean"; + return CLEAN_HEADER; + } + } + + static const std::string authorization("authorization"); + if (memcaseinsensitivecmp(name, name_len, authorization.data(), authorization.size())) { + dbgTrace(D_WAAP) << "[transaction:" << this << "] special header '" << std::string(name, name_len) << + "' - detect base64 to determine cleanliness ..."; + + std::string result; + int decodedCount = 0; + int deletedCount = 0; + + std::string v(value, value_len); + boost::algorithm::to_lower(v); + const std::string negotiate("negotiate "); + + if (boost::algorithm::starts_with(v, negotiate)) { + v = v.substr(negotiate.size(), v.size() - negotiate.size()); + + // Detect potential base64 match after the "Negotiate " prefix + Waap::Util::b64Decode(v, b64DecodeChunk, decodedCount, deletedCount, result); + if (result.empty() && (deletedCount + decodedCount == 1)) { + // Decoded 1 base64 chunk and nothing left behind it + dbgTrace(D_WAAP) << "[transaction:" << this << "] special header '" << + std::string(name, name_len) << " is clean"; + return CLEAN_HEADER; + } + } + } + + } + return UNKNOWN_HEADER; +} + +// Methods below are callbacks that are called during HTTP transaction processing by the front-end server/proxy +void Waf2Transaction::start() { + dbgTrace(D_WAAP) << "[Waf2Transaction::start():" << this << "] start"; + // TODO:: maybe guard against double call of this function by buggy client. + m_contentType = Waap::Util::CONTENT_TYPE_UNKNOWN; + m_remote_addr.clear(); + m_remote_port = 0; + m_local_addr.clear(); + m_local_port = 0; + m_request_body_bytes_received = 0; + m_response_body_bytes_received = 0; + m_requestBodyParser = NULL; + m_methodStr.clear(); + m_uriStr.clear(); + m_uriPath.clear(); + m_uriReferer.clear(); + m_uriQuery.clear(); + m_contentTypeStr.clear(); + m_hostStr.clear(); + m_userAgentStr.clear(); + m_cookieStr.clear(); + m_notes.clear(); + m_source_identifier.clear(); + // TODO:: remove this! refactor extraction of kv_pairs! + m_deepParser.clear(); + hdrs_map.clear(); + m_request_body.clear(); + m_response_body.clear(); +} + +void Waf2Transaction::set_transaction_time(const char* log_time) { + dbgTrace(D_WAAP) << "[transaction:" << this << "] set_transaction_time(log_time='" << log_time << "')"; + m_log_time = log_time; +} + +void Waf2Transaction::set_transaction_remote(const char* remote_addr, int remote_port) { + dbgTrace(D_WAAP) << "[transaction:" << this << "] set_transaction_remote('" << remote_addr << ":" << remote_port << + "')"; + m_remote_addr = remote_addr; + m_remote_port = remote_port; + m_source_identifier = remote_addr; +} + +void Waf2Transaction::set_transaction_local(const char* local_addr, int local_port) { + dbgTrace(D_WAAP) << "[transaction:" << this << "] set_transaction_local('" << local_addr << ":" << local_port << + "')"; + m_local_addr = local_addr; + m_local_port = local_port; +} + +void Waf2Transaction::set_method(const char* method) { + + dbgTrace(D_WAAP) << "[transaction:" << this << "] set_method('" << method << "')"; + m_methodStr = method; +} + +bool Waf2Transaction::checkIsScanningRequired() +{ + bool result = false; + if (WaapConfigAPI::getWaapAPIConfig(m_ngenAPIConfig)) { + m_siteConfig = &m_ngenAPIConfig; + auto rateLimitingPolicy = m_siteConfig ? m_siteConfig->get_RateLimitingPolicy() : NULL; + result |= m_siteConfig->get_WebAttackMitigation(); + if(rateLimitingPolicy) { + result |= m_siteConfig->get_RateLimitingPolicy()->getRateLimitingEnforcementStatus(); + } + + auto userLimitsPolicy = m_siteConfig ? m_siteConfig->get_UserLimitsPolicy() : nullptr; + if (userLimitsPolicy) { + result = true; + } + } + + if (WaapConfigApplication::getWaapSiteConfig(m_ngenSiteConfig)) { + m_siteConfig = &m_ngenSiteConfig; + auto rateLimitingPolicy = m_siteConfig ? m_siteConfig->get_RateLimitingPolicy() : NULL; + auto errorLimitingPolicy = m_siteConfig ? m_siteConfig->get_ErrorLimitingPolicy() : NULL; + auto csrfPolicy = m_siteConfig ? m_siteConfig->get_CsrfPolicy() : NULL; + auto userLimitsPolicy = m_siteConfig ? m_siteConfig->get_UserLimitsPolicy() : nullptr; + result |= m_siteConfig->get_WebAttackMitigation(); + + if (rateLimitingPolicy) { + result |= m_siteConfig->get_RateLimitingPolicy()->getRateLimitingEnforcementStatus(); + } + if (errorLimitingPolicy) { + result |= m_siteConfig->get_ErrorLimitingPolicy()->getRateLimitingEnforcementStatus(); + } + if (csrfPolicy) { + result |= m_siteConfig->get_CsrfPolicy()->enable; + } + if (userLimitsPolicy) { + result = true; + } + } + return result; +} + +bool Waf2Transaction::setCurrentAssetContext() +{ + // the return value tells me if I need to scan traffic + bool result = false; + m_siteConfig = NULL; + + result |= checkIsScanningRequired(); + + if (!m_siteConfig) { + dbgWarning(D_WAAP) << "[transaction:" << this << "] " + "Failed to set sitePolicy for asset... using the original signatures"; + return result; + } + + setCurrentAssetState(m_siteConfig); + m_deepParser.setWaapAssetState(m_pWaapAssetState); + m_pWaapAssetState->updateFilterManagerPolicy(m_siteConfig); + m_pWaapAssetState->clearFilterVerbose(); + + return result; +} + +void Waf2Transaction::processUri(const char* uri, const std::string& scanStage) { + m_processedUri = true; + const char* p = uri; + std::string baseUri; + + // TODO:: refactor out this block to method, and the next block (parsing url parameters), too. + { + bool pushed = false; + bool firstPush = true; + + // Parse URL + ParserRaw urlParser(m_deepParserReceiver, scanStage); + + // Scan the uri until '?' character found (or until end of the uri string). + do { + const char* q = strchr(p, '?'); + + if (q == NULL) { + // Handle special case found in customer traffic where instead of '?' there was ';' character. + q = strchr(p, ';'); + if (q) { + // Check that after ';' the parameter name is valid and terminated with '='. This would normally be + // the case in legit traffic, but not in attacks. This covers a case of "sap login". + const char *qq; + for (qq = q + 1; isalpha(*qq) || isdigit(*qq) || *qq=='-' || *qq=='_' || *qq=='*'; ++qq); + if (*qq != '=') { + // Assume it might be attack and cancel the separation by the ';' character (scan whole URL) + q = NULL; + } + } + } + + if (q == NULL) { + baseUri = std::string(p); + if (scanStage == "url") { + m_uriPath = baseUri; + } + if (firstPush) { + dbgTrace(D_WAAP) << "[transaction:" << this << "] scanning the " << scanStage.c_str(); + firstPush = false; + } + + // Push the last piece to URL scanner + pushed = true; + std::string url(p, strlen(p)); + + Waap::Util::decodePercentEncoding(url); + urlParser.push(url.data(), url.size()); + + // We found no '?' character so set p to NULL to prevent parameters scan below. + p = NULL; + break; + } + + baseUri = std::string(p, q - p); + if (scanStage == "url") { + m_uriPath = baseUri; + } + + // Push data between last point (p) and the character we found ('?'), not includig the character. + if (q != p) { + // Just so we print this trace message only once + if (firstPush) { + dbgTrace(D_WAAP) << "[transaction:" << this << "] scanning the " << scanStage.c_str(); + firstPush = false; + } + + pushed = true; + std::string url(p, q-p); + Waap::Util::decodePercentEncoding(url); + urlParser.push(url.data(), url.size()); + } + + // If we hit the '?' character, finish parsing the URL and continue parsing URL + // parameters from the character next to '?' + p = q + 1; + break; + } while (1); + + if (pushed) { + urlParser.finish(); + m_notes.push_back(scanStage + "_scanned"); + } + } + // in case we found any indication in one of the URI segments and there is not one that starts with / + // scan the whole URI + if (m_scanResult && m_scanResult->score != 0 && (m_scanResult->location == scanStage) && + std::find_if(m_scanResult->keyword_matches.begin(), + m_scanResult->keyword_matches.end(), [](std::string keyword) { return keyword[0] == '/'; }) == + m_scanResult->keyword_matches.end()) + { + auto scanResultBackup = m_scanResult; + m_scanResult = nullptr; + bool ignoreScore = m_ignoreScore; + m_ignoreScore = true; + m_deepParser.m_key.push(scanStage.c_str(), scanStage.size()); + ParserDelimiter uriSegmentsParser(m_deepParserReceiver, '/', scanStage); + std::string baseUriUnescaped(baseUri); + Waap::Util::decodePercentEncoding(baseUriUnescaped); + uriSegmentsParser.push(baseUriUnescaped.c_str(), baseUriUnescaped.length()); + uriSegmentsParser.finish(); + m_deepParser.m_key.pop(scanStage.c_str()); + m_ignoreScore = ignoreScore; + if (uriSegmentsParser.error()) + { + // handle special case where there is no / in the URI - can happen in attackes + m_deepParserReceiver.clear(); + delete m_scanResult; + m_scanResult = scanResultBackup; + } + else { + if (m_scanResult) + { + // keep original scan of the whole URL + delete m_scanResult; + m_scanResult = scanResultBackup; + } + else + { + // scan result is empty when we parsing each segments + // i.e. scan result from using (acceptable) irregular format in the URI - discarding the original scan + delete scanResultBackup; + } + } + } + // at this point, p can either be NULL (if there are no URL parameters), + // or point to the parameters string (right after the '?' character) + + if (p && *p) { + // Decode URLEncoded data and send decoded key/value pairs to deep inspection + dbgTrace(D_WAAP) << "[transaction:" << this << "] scanning the " << scanStage.c_str() << " parameters"; + + if (scanStage == "url") { + m_uriQuery = std::string(p); + } + + std::string tag = scanStage + "_param"; + m_deepParser.m_key.push(tag.data(), tag.size()); + size_t buff_len = strlen(p); + ParserUrlEncode up(m_deepParserReceiver, '&', checkUrlEncoded(p, buff_len)); + up.push(p, buff_len); + up.finish(); + m_deepParser.m_key.pop(tag.c_str()); + m_notes.push_back(scanStage + "_params_scanned"); + } +} + +void Waf2Transaction::parseContentType(const char* value, int value_len) +{ + // content type header parser + ContentTypeParser ctp; + + ctp.push(value, value_len); + ctp.finish(); + + dbgTrace(D_WAAP) << "[transaction:" << this << "] ctp detected content type: '" << + ctp.contentTypeDetected.c_str() << "'"; + // The above fills m_contentTypeDetected + m_contentType = Waap::Util::detectContentType(ctp.contentTypeDetected.c_str()); + + // extract boundary string required for parsing multipart-form-data stream + if (m_contentType == Waap::Util::CONTENT_TYPE_MULTIPART_FORM) { + dbgTrace(D_WAAP) << "content_type detected: " << Waap::Util::getContentTypeStr(m_contentType) << + "; boundary='" << ctp.boundaryFound.c_str() << "'"; + m_deepParser.setMultipartBoundary(ctp.boundaryFound); + } + else { + dbgTrace(D_WAAP) << "content_type detected: " << Waap::Util::getContentTypeStr(m_contentType); + } + + std::string contentTypeFull(value, value_len); + // Use content-type trimmed by the first ';' character + m_contentTypeStr = contentTypeFull.substr(0, contentTypeFull.find(";")); +} + +void Waf2Transaction::parseCookie(const char* value, int value_len) +{ + m_cookieStr = std::string(value, value_len); + +#ifdef NO_HEADERS_SCAN + return; +#endif + + if (value_len > 0) { + dbgTrace(D_WAAP) << "[transaction:" << this << "] scanning the cookie value"; + m_deepParser.m_key.push("cookie", 6); + ParserUrlEncode cookieValueParser(m_deepParserReceiver, ';'); + cookieValueParser.push(value, value_len); + cookieValueParser.finish(); + m_deepParser.m_key.pop("cookie"); + m_notes.push_back("cookie_scanned"); + } +} + +void Waf2Transaction::parseReferer(const char* value, int value_len) +{ +#ifdef NO_HEADERS_SCAN + return; +#endif + dbgTrace(D_WAAP) << "Parsed Referer. Referer URI: " << m_uriReferer; + + std::string referer(value, value_len); + std::vector regex_matches; + size_t uriParsedElements = + m_pWaapAssetState->getSignatures()->uri_parser_regex.findAllMatches(referer, regex_matches); + if(uriParsedElements > 0) + { + RegexMatch::MatchGroup& uriPathGroup = regex_matches[0].groups[3]; + m_uriReferer = uriPathGroup.value; + m_uriReferer = normalize_uri(m_uriReferer); + } + // Parse referer value as if it was a URL + if (value_len > 0) + { + processUri(std::string(value, value_len).c_str(), "referer"); + } +} + +void Waf2Transaction::parseUnknownHeaderName(const char* name, int name_len) +{ +#ifdef NO_HEADERS_SCAN + return; +#endif + // Apply signatures on all other, header names, unless they are considered "good" ones to skip scanning them. + if (name_len && + !m_pWaapAssetState->getSignatures()->good_header_name_re.hasMatch(std::string(name, name_len))) { + dbgTrace(D_WAAP) << "[transaction:" << this << "] scanning the header name"; + m_deepParser.m_key.push("header", 6); + ParserRaw headerNameParser(m_deepParserReceiver, std::string(name, name_len)); + headerNameParser.push(name, name_len); + headerNameParser.finish(); + m_deepParser.m_key.pop("header name"); + m_notes.push_back("hn:" + std::string(name, name_len)); + } +} + +void Waf2Transaction::parseGenericHeaderValue(const std::string &headerName, const char* value, int value_len) +{ +#ifdef NO_HEADERS_SCAN + return; +#endif + if (value_len == 0) { + return; + } + + dbgTrace(D_WAAP) << "[transaction:" << this << "] scanning the header value"; + m_deepParser.m_key.push("header", 6); + ParserRaw headerValueParser(m_deepParserReceiver, headerName); + headerValueParser.push(value, value_len); + headerValueParser.finish(); + m_deepParser.m_key.pop("header value"); + m_notes.push_back("hv:" + headerName); +}; + +// Scan relevant headers to detect attacks inside them +void Waf2Transaction::scanSpecificHeder(const char* name, int name_len, const char* value, int value_len) +{ + HeaderType header_t = detectHeaderType(name, name_len); + std::string headerName = std::string(name, name_len); + + switch (header_t) + { + case HeaderType::COOKIE_HEADER: + parseCookie(value, value_len); + break; + case HeaderType::REFERER_HEADER: + parseReferer(value, value_len); + break; + case HeaderType::UNKNOWN_HEADER: { + HeaderType headerType = checkCleanHeader(name, name_len, value, value_len); + if(headerType == HeaderType::CLEAN_HEADER) { + break; + } + // Scan names of all unknown headers + parseUnknownHeaderName(name, name_len); + // Scan unknown headers whose values do not match "clean generic header" pattern. + // Note that we do want to process special header named x-chkp-csrf-token header - it is treated specially. + if (!m_pWaapAssetState->getSignatures()->good_header_value_re.hasMatch(std::string(value, value_len)) || + headerName == "x-chkp-csrf-token" || headerType == HeaderType::OTHER_KNOWN_HEADERS) { + parseGenericHeaderValue(headerName, value, value_len); + } + break; + } + case HeaderType::USER_AGENT_HEADER: { + HeaderType headerType = checkCleanHeader(name, name_len, value, value_len); + if(headerType == HeaderType::CLEAN_HEADER) { + break; + } + // In case the user agent header contains a known regex match, remove the match before scanning + std::string hdrValue(value, value_len); + hdrValue = NGEN::Regex::regexReplace( + __FILE__, + __LINE__, + hdrValue, + m_pWaapAssetState->getSignatures()->user_agent_prefix_re, + "" + ); + parseGenericHeaderValue(headerName, hdrValue.data(), hdrValue.size()); + break; + } + case HeaderType::CONTENT_TYPE_HEADER: { + HeaderType headerType = checkCleanHeader(name, name_len, value, value_len); + if(headerType == HeaderType::CLEAN_HEADER) { + break; + } + // Parsing of a known header will only take place if its value does not match strict rules and is therefore + // suspected to contain an attack + parseGenericHeaderValue(headerName, value, value_len); + break; + } + default: + break; + } +}; + +// Read headers to extract information from them (like hostname from the Host: header). Do not scan them for attacks. +void Waf2Transaction::detectSpecificHeader(const char* name, int name_len, const char* value, int value_len) +{ + HeaderType header_t = detectHeaderType(name, name_len); + + switch (header_t) + { + case HeaderType::CONTENT_TYPE_HEADER: + parseContentType(value, value_len); + break; + case HeaderType::HOST_HEADER: + m_hostStr = std::string(value, value_len); + break; + case HeaderType::USER_AGENT_HEADER: + m_userAgentStr = std::string(value, value_len); + break; + default: + break; + } +} + +void Waf2Transaction::detectHeaders() +{ + if (isUrlLimitReached(m_uriStr.size())) { + dbgTrace(D_WAAP_ULIMITS) << "[USER LIMITS] Url limit exceeded"; + return; + } + else if (!isPreventModeValidMethod(getMethod())) { + dbgTrace(D_WAAP_ULIMITS) << "[USER LIMITS] Invalid http method: " << getMethod(); + return; + } + + for (auto it = hdrs_map.begin(); it != hdrs_map.end(); ++it) + { + if (isHttpHeaderLimitReached(it->first, it->second)) { + dbgTrace(D_WAAP_ULIMITS) << "[USER LIMITS] Http header limit exceeded"; + return; + } + detectSpecificHeader(it->first.c_str(), it->first.size(), + it->second.c_str(), it->second.size()); + } +} + +void Waf2Transaction::scanHeaders() +{ + m_processedHeaders = true; + + // Scan relevant headers for attacks + for (auto it = hdrs_map.begin(); it != hdrs_map.end(); ++it) + { + scanSpecificHeder(it->first.c_str(), it->first.size(), + it->second.c_str(), it->second.size()); + } +} + +void Waf2Transaction::set_uri(const char* uri) { + dbgTrace(D_WAAP) << "[transaction:" << this << "] set_uri('" << uri << "')"; + m_uriStr = uri; +} + +void Waf2Transaction::set_host(const char* host) { + dbgTrace(D_WAAP) << "[transaction:" << this << "] set_host('" << host << "')"; + m_hostStr = host; +} + +void Waf2Transaction::start_request_hdrs() { + dbgTrace(D_WAAP) << "[transaction:" << this << "] start_request_hdrs"; + // Clear all things that will be filled by the incoming request headers that will follow + m_contentType = Waap::Util::CONTENT_TYPE_UNKNOWN; + m_requestBodyParser = NULL; +} + +void Waf2Transaction::add_request_hdr(const char* name, int name_len, const char* value, int value_len) { + dbgTrace(D_WAAP) << "[transaction:" << this << "] add_request_hdr(name='" << std::string(name, name_len) << + "', value='" << std::string(value, value_len) << "')"; + std::string header_name(name, name_len); + boost::algorithm::to_lower(header_name); + hdrs_map[header_name] = std::string(value, value_len); +} + +void Waf2Transaction::end_request_hdrs() { + + dbgFlow(D_WAAP) << "[transaction:" << this << "] end_request_hdrs"; + m_isScanningRequired = setCurrentAssetContext(); + if (m_siteConfig != NULL) + { + // getOverrideState also extracts the source identifier and populates m_source_identifier + // but the State itself is not needed now + Waap::Override::State overrideState = getOverrideState(m_siteConfig); + } + IdentifiersEvent ids(m_source_identifier, m_pWaapAssetState->m_assetId); + ids.notify(); + // Read relevant headers and extract meta information such as host name + // Do this before scanning the URL because scanning URL might require this information. + if (m_isScanningRequired) { + createUserLimitsState(); + detectHeaders(); + if (isUserLimitReached()) { + return; + } + } + // Scan URL and url query + if (m_isScanningRequired && !m_processedUri) { + processUri(m_uriStr.c_str(), "url"); + } + // Scan relevant headers for attacks + if (m_isScanningRequired && !m_processedHeaders) { + scanHeaders(); + } + + if(m_siteConfig != NULL) { + // Create rate limiting policy (lazy, on first request) + const std::shared_ptr rateLimitingPolicy = m_siteConfig->get_RateLimitingPolicy(); + if(rateLimitingPolicy && rateLimitingPolicy->getRateLimitingEnforcementStatus()) + { + if (m_pWaapAssetState->getRateLimitingState() == nullptr) + { + m_pWaapAssetState->createRateLimitingState(rateLimitingPolicy); + } + dbgTrace(D_WAAP) << "(Waf2Engine::end_request_hdrs): RateLimiting check starts."; + + // Get current clock time + I_TimeGet* timer = Singleton::Consume::by(); + + // The rate limiting state tracks rate limiting information for all sources + std::shared_ptr rateLimitingState = m_pWaapAssetState->getRateLimitingState(); + + std::chrono::seconds now = std::chrono::duration_cast(timer->getMonotonicTime()); + + bool logRateLimiting = false; + if (rateLimitingState && (rateLimitingState->execute + (m_source_identifier, m_uriPath, now, logRateLimiting) == false)) + { + dbgTrace(D_WAAP) << "(Waf2Engine::end_request_hdrs): RateLimiting decision: Block."; + // block request due to rate limiting + auto decision = m_waapDecision.getDecision(RATE_LIMITING_DECISION); + decision->setBlock(true); + decision->setLog(logRateLimiting); + } + } + else { + dbgTrace(D_WAAP) << "(Waf2Engine::end_request_hdrs): No rate limiting policy."; + } + } +} + +void Waf2Transaction::start_request_body() { + dbgTrace(D_WAAP) << "[transaction:" << this << "] start_request_body: m_contentType=" << m_contentType; + + clearRequestParserState(); + + m_requestBodyParser = new ParserRaw(m_deepParserReceiver, "body"); + + m_request_body_bytes_received = 0; + m_request_body.clear(); +} + +void Waf2Transaction::add_request_body_chunk(const char* data, int data_len) { + dbgTrace(D_WAAP) << "[transaction:" << this << "] add_request_body_chunk (" << data_len << " bytes): parser='" << + (m_requestBodyParser ? m_requestBodyParser->name() : "none") << "': '" << std::string(data, data_len) << "'"; + + if (isHttpBodyLimitReached(data_len)) { + dbgTrace(D_WAAP_ULIMITS) << "[USER LIMITS] Http body limit exceeded"; + return; + } + m_request_body_bytes_received += data_len; + size_t maxSizeToScan = m_request_body_bytes_received; + + if (m_siteConfig != NULL) + { + auto waapParams = m_siteConfig->get_WaapParametersPolicy(); + if (waapParams != nullptr) + { + std::string maxSizeToScanStr = waapParams->getParamVal("max_body_size", ""); + if (maxSizeToScanStr != "") + { + maxSizeToScan = std::stoul(maxSizeToScanStr.c_str()); + } + } + } + + if (m_isScanningRequired && m_request_body_bytes_received <= maxSizeToScan) + { + if (m_requestBodyParser != NULL) { + m_requestBodyParser->push(data, data_len); + if (isObjectDepthLimitReached(m_deepParser.getLocalMaxObjectDepth())) { + dbgTrace(D_WAAP_ULIMITS) << "[USER LIMITS] Object depth limit exceeded"; + return; + } + } + else { + dbgWarning(D_WAAP) << "[transaction:" << this << "] add_request_body_chunk (" << data_len << + " bytes): parser='NONE'. This is most probably a bug. " + "Some parser MUST be installed for this transaction!"; + } + } + + // Collect up to MAX_REQUEST_BODY_SIZE of input data for each request + if (m_request_body.length() + data_len <= MAX_REQUEST_BODY_SIZE) { + m_request_body.append(data, (size_t)data_len); + } + else if (m_request_body.length() < MAX_REQUEST_BODY_SIZE) { + size_t piece = MAX_REQUEST_BODY_SIZE - m_request_body.length(); + // Note: piece is guaranteed to be > data_len, so the write below is safe. + m_request_body.append(data, piece); + } +} + +void Waf2Transaction::end_request_body() { + dbgTrace(D_WAAP) << "[transaction:" << this << "] end_request_body"; + + if (m_requestBodyParser != NULL) { + m_requestBodyParser->finish(); + if (isObjectDepthLimitReached(m_deepParser.getLocalMaxObjectDepth())) { + dbgTrace(D_WAAP_ULIMITS) << "[USER LIMITS] Object depth limit exceeded"; + } + + if (m_contentType != Waap::Util::CONTENT_TYPE_UNKNOWN && m_request_body.length() > 0) { + m_deepParser.m_key.pop("body"); + } + } + + // Check and output [ERROR] message if keyStack is not empty (it should be empty here). + if (!m_deepParser.m_key.empty()) { + dbgWarning(D_WAAP) << "[transaction:" << this << "] end_request_body: parser='" << + (m_requestBodyParser ? m_requestBodyParser->name() : "") << + "'. ERROR: m_key is not empty. full key='" << m_deepParser.m_key.c_str() << "'"; + } + + clearRequestParserState(); +} + +void Waf2Transaction::end_request() { + dbgTrace(D_WAAP) << "[transaction:" << this << "] end_request"; + clearRequestParserState(); + + // Enable response headers processing only if values parsed from request contained at least one URL + auto openRedirectPolicy = m_siteConfig ? m_siteConfig->get_OpenRedirectPolicy() : NULL; + if (openRedirectPolicy && openRedirectPolicy->enable && !m_openRedirectState.empty()) { + m_responseInspectReasons.setOpenRedirect(true); + } + + auto errorLimitingPolicy = m_siteConfig ? m_siteConfig->get_ErrorLimitingPolicy() : NULL; + if (errorLimitingPolicy && errorLimitingPolicy->getRateLimitingEnforcementStatus()) { + m_responseInspectReasons.setErrorLimiter(true); + } + + auto rateLimitingPolicy = m_siteConfig ? m_siteConfig->get_RateLimitingPolicy() : NULL; + if (rateLimitingPolicy && rateLimitingPolicy->getRateLimitingEnforcementStatus()) { + m_responseInspectReasons.setRateLimiting(true); + } + + auto securityHeadersPolicy = m_siteConfig ? m_siteConfig->get_SecurityHeadersPolicy() : NULL; + if (securityHeadersPolicy && securityHeadersPolicy->m_securityHeaders.enable) { + m_responseInjectReasons.setSecurityHeaders(true); + if (m_pWaapAssetState->getSecurityHeadersState() == nullptr) + { + m_pWaapAssetState->createSecurityHeadersState(securityHeadersPolicy); + } + dbgTrace(D_WAAP) << "(Waf2Engine::end_request): Security Headers State was created"; + } + + // Enable response headers processing if response scanning is enabled in policy + auto errorDisclosurePolicy = m_siteConfig ? m_siteConfig->get_ErrorDisclosurePolicy() : NULL; + m_responseInspectReasons.setErrorDisclosure(errorDisclosurePolicy && errorDisclosurePolicy->enable); +} + +void Waf2Transaction::extractEnvSourceIdentifier() +{ + auto env = Singleton::Consume::by(); + auto env_source_identifiers = env->get("sourceIdentifiers"); + if (!env_source_identifiers.ok() || env_source_identifiers.unpack().empty()) { + dbgInfo(D_WAAP) << "Could not extract source identifier from the environment"; + return; + } + + // Take the first source identifier in set provided by the environment + dbgTrace(D_WAAP) << "Set source identifier from the Environment"; + m_source_identifier = *(env_source_identifiers); +} + +void Waf2Transaction::finish() { + dbgTrace(D_WAAP) << "[transaction:" << this << "] finish"; + clearRequestParserState(); +} + +void Waf2Transaction::set_ignoreScore(bool ignoreScore) { + m_ignoreScore = ignoreScore; +} + +void +Waf2Transaction::decide( + bool& bForceBlock, + bool& bForceException, + int mode) +{ + dbgTrace(D_WAAP) << "[transaction:" << this << "] decide (m_scanResult=" << m_scanResult << ")..."; + + int bSendResponse = false; + + // If WAF stage1 found suspicious request - send it to stage2 and wait for decision. + if (m_scanResult) { + bSendResponse = true; + } + + // If mode == 2 - don't send all traffic to stage2 (it won't be logged) + if (mode == 2) { + bSendResponse = false; + } + + // Normalize URL + std::string normalizedUri = normalize_uri(m_uriStr); + + std::string json = buildWaapResultJson( + m_scanResult, + *this, + bSendResponse, + normalizedUri, + m_uriStr, + bForceBlock, + bForceException + ); + m_waapDecision.setJson(json); +} + +bool +Waf2Transaction::isHtmlType(const char* data, int data_len){ + if(m_uriPath.find(".js") != std::string::npos || m_uriPath.find(".css") != std::string::npos) + { + dbgTrace(D_WAAP) << "Waf2Transaction::isHtmlType: false"; + return false; + } + std::string body(data); + if(!m_pWaapAssetState->getSignatures()->html_regex.hasMatch(body)) + { + dbgTrace(D_WAAP) << "Waf2Transaction::isHtmlType: false"; + return false; + } + dbgTrace(D_WAAP) << "Waf2Transaction::isHtmlType: true"; + return true; +} + +// Search for html tag - return true if found and update the injection correct position. +bool +Waf2Transaction::findHtmlTagToInject(const char* data, int data_len, int& pos) +{ + bool headFound = false; + static const char tag[] = ""; + static size_t tagSize = sizeof(tag) - 1; + + // Searching tag by iterating over data and always check last 6 bytes against the required tag. + for (pos = 0; pos= tagSize) { + m_tagHistPos = 0; + } + // check + bool tagMatches = true; + size_t tagHistPosCheck = m_tagHistPos; + for (size_t i=0; i < tagSize; ++i) { + if (tag[i] != ::tolower(m_tagHist[tagHistPosCheck])) { + tagMatches = false; + break; + } + tagHistPosCheck++; + if (tagHistPosCheck >= tagSize) { + tagHistPosCheck = 0; + } + } + if (tagMatches) { + headFound = true; + } + } + + if(!headFound) + { + return false; + } + + return true; +} + +void +Waf2Transaction::completeInjectionResponseBody(std::string& strInjection) +{ + if (m_responseInjectReasons.shouldInjectAntibot()) { + dbgTrace(D_WAAP_BOT_PROTECTION) << + "Waf2Transaction::completeInjectionResponseBody(): Injecting data (antiBot)"; + strInjection += ""; + // No need to inject more than once + m_responseInjectReasons.setAntibot(false); + } + + if (m_responseInjectReasons.shouldInjectCsrf()) { + dbgTrace(D_WAAP) << "Waf2Transaction::completeInjectionResponseBody(): Injecting data (csrf)"; + strInjection += ""; + // No need to inject more than once + m_responseInjectReasons.setCsrf(false); + } +} + +void +Waf2Transaction::handleSecurityHeadersInjection(std::vector>& injectHeaderStrs){ + auto securityHeadersPolicy = m_siteConfig ? m_siteConfig->get_SecurityHeadersPolicy() : NULL; + if (securityHeadersPolicy) { + if (!securityHeadersPolicy->m_securityHeaders.enable) { + dbgTrace(D_WAAP) << + "(Waf2Engine::handleSecurityHeadersInjection): Security Headers Disabled"; + } + else if (m_pWaapAssetState->getSecurityHeadersState() == nullptr) { + dbgDebug(D_WAAP) << + "(Waf2Engine::handleSecurityHeadersInjection): Security Headers State was not created as expected"; + } + else { + injectHeaderStrs = m_pWaapAssetState->getSecurityHeadersState()->headersInjectStrs; + } + } +} + +bool Waf2Transaction::shouldInjectCSRF() +{ + return m_responseInjectReasons.shouldInjectCsrf(); +} + +void Waf2Transaction::disableShouldInjectSecurityHeaders() { + m_responseInjectReasons.setSecurityHeaders(false); +} + +bool Waf2Transaction::shouldInjectSecurityHeaders() +{ + return m_responseInjectReasons.shouldInjectSecurityHeaders(); +} + +void +Waf2Transaction::checkShouldInject() +{ + dbgTrace(D_WAAP) << "Waf2Transaction::checkShouldInject(): starts"; + std::string uri = m_uriPath; + std::string low_method = m_methodStr; + std::transform(low_method.begin(), low_method.end(), low_method.begin(), ::tolower); + + auto csrfPolicy = m_siteConfig ? m_siteConfig->get_CsrfPolicy() : NULL; + bool csrf = false; + dbgTrace(D_WAAP) << "Waf2Transaction::checkShouldInject(): received the relevant Application configuration " + "from the I/S"; + if (csrfPolicy && csrfPolicy->enable) { + csrf = true; + } + else + { + dbgTrace(D_WAAP) << "Waf2Transaction::checkShouldInject(): Should not inject CSRF scripts."; + } + + if(csrf) { + dbgTrace(D_WAAP) << "Waf2Transaction::checkShouldInject(): Should inject CSRF script"; + m_responseInjectReasons.setCsrf(true); + } + return; +} + +bool +Waf2Transaction::decideAfterHeaders() +{ + dbgFlow(D_WAAP) << "Waf2Transaction::decideAfterHeaders()"; + + WaapConfigAPI ngenAPIConfig; + WaapConfigApplication ngenSiteConfig; + IWaapConfig *sitePolicy = NULL; // will be NULL or point to either API or SITE config. + + if (WaapConfigAPI::getWaapAPIConfig(ngenAPIConfig)) { + dbgTrace(D_WAAP) << "Waf2Transaction::decideAfterHeaders(): got relevant API configuration from the I/S"; + sitePolicy = &ngenAPIConfig; + } + else if (WaapConfigApplication::getWaapSiteConfig(ngenSiteConfig)) { + dbgTrace(D_WAAP) << + "Waf2Transaction::decideAfterHeaders(): got relevant Application configuration from the I/S"; + sitePolicy = &ngenSiteConfig; + } + + if (!sitePolicy) { + dbgTrace(D_WAAP) << "Waf2Transaaction::decideAfterHeaders(): no policy - do not block"; + return false; + } + + m_overrideState = getOverrideState(sitePolicy); + + // Select scores pool by location (but use forced pool when forced) + std::string realPoolName = + (m_scanResult) ? + Waap::Scores::getScorePoolNameByLocation(m_scanResult->location) : + KEYWORDS_SCORE_POOL_BASE; + + // Autonomus Security + AnalysisResult analysisResult; + bool shouldBlock = decideAutonomousSecurity( + *sitePolicy, + 1, + true, + analysisResult, + realPoolName, + UNKNOWN_TYPE + ); + + return finalizeDecision(sitePolicy, shouldBlock); +} + +// Note: the only user of the transactionResult structure filled by this method is waap_automation. +// TODO: Consider removing this parameter (and provide access to this information by other means) +int +Waf2Transaction::decideFinal( + int mode, + AnalysisResult &transactionResult, + const std::string &poolName, + PolicyCounterType fpClassification) +{ + dbgFlow(D_WAAP) << "Waf2Transaction::decideFinal(): starts"; + + // Select scores pool by location (but use forced pool when forced) + std::string realPoolName = + (poolName == KEYWORDS_SCORE_POOL_BASE && m_scanResult) ? + Waap::Scores::getScorePoolNameByLocation(m_scanResult->location) : + poolName; + + // decision of (either) API or Application module + bool shouldBlock = false; + + // TODO:: base class for both, with common inteface + WaapConfigAPI ngenAPIConfig; + WaapConfigApplication ngenSiteConfig; + IWaapConfig *sitePolicy = NULL; // will be NULL or point to either API or SITE config. + + // API config is more specific, hence if it exists it overrides anything from WaapConfigApplication + if (WaapConfigAPI::getWaapAPIConfig(ngenAPIConfig)) { + dbgTrace(D_WAAP) << "Waf2Transaction::decideFinal(): got relevant API configuration from the I/S"; + sitePolicy = &ngenAPIConfig; + m_overrideState = getOverrideState(sitePolicy); + + // User limits + shouldBlock = (getUserLimitVerdict() == ngx_http_cp_verdict_e::TRAFFIC_VERDICT_DROP); + } + else if (WaapConfigApplication::getWaapSiteConfig(ngenSiteConfig)) { + dbgTrace(D_WAAP) << "Waf2Transaction::decideFinal(): got relevant Application configuration from the I/S"; + sitePolicy = &ngenSiteConfig; + m_overrideState = getOverrideState(sitePolicy); + + // Autonomus Security + shouldBlock = decideAutonomousSecurity( + *sitePolicy, + mode, + false, + transactionResult, + realPoolName, + fpClassification + ); + // CSRF Protection + auto csrfPolicy = m_siteConfig ? m_siteConfig->get_CsrfPolicy() : nullptr; + if(csrfPolicy && csrfPolicy->enable) { + shouldBlock |= m_csrfState.decide(m_methodStr, m_waapDecision, csrfPolicy); + } + // User limits + shouldBlock |= (getUserLimitVerdict() == ngx_http_cp_verdict_e::TRAFFIC_VERDICT_DROP); + } + + if (mode == 2) { + decide( + m_overrideState.bForceBlock, + m_overrideState.bForceException, + mode + ); + shouldBlock = isSuspicious(); + } + + return finalizeDecision(sitePolicy, shouldBlock); +} + +int +Waf2Transaction::finalizeDecision(IWaapConfig *sitePolicy, bool shouldBlock) +{ + auto decision = std::dynamic_pointer_cast( + m_waapDecision.getDecision(AUTONOMOUS_SECURITY_DECISION)); + // Send log + if (sitePolicy) + { + // auto reject should have default threat level info and above + if (m_overrideState.bForceBlock && decision->getThreatLevel() == ThreatLevel::NO_THREAT) + { + decision->setThreatLevel(ThreatLevel::THREAT_INFO); + } + } + + if (m_overrideState.bForceBlock) { + dbgTrace(D_WAAP) << "Waf2Transaction::finalizeDecision(): setting shouldBlock to true due to override"; + shouldBlock = true; // BLOCK + } + else if (m_overrideState.bForceException) { + dbgTrace(D_WAAP) << "Waf2Transaction::finalizeDecision(): setting shouldBlock to false due to override"; + shouldBlock = false; // PASS + } + + if (m_siteConfig) { + const std::shared_ptr triggerPolicy = m_siteConfig->get_TriggerPolicy(); + if (triggerPolicy) { + const std::shared_ptr triggerLog = getTriggerLog(triggerPolicy); + if (triggerLog && shouldSendExtendedLog(triggerLog)) + { + m_responseInspectReasons.setCollectResponseForLog(true); + } + } + } + + dbgTrace(D_WAAP) << "Waf2Transaction::finalizeDecision(): returning shouldBlock: " << shouldBlock; + return shouldBlock; +} + +void Waf2Transaction::appendCommonLogFields(LogGen& waapLog, + const std::shared_ptr &triggerLog, + bool shouldBlock, + const std::string& logOverride, + const std::string& incidentType) const +{ + auto env = Singleton::Consume::by(); + auto proxy_ip = env->get(HttpTransactionData::proxy_ip_ctx); + if (proxy_ip.ok() && m_remote_addr != proxy_ip.unpack()) + { + waapLog << LogField("proxyIP", static_cast(proxy_ip.unpack())); + } + waapLog << LogField("sourceIP", m_remote_addr); + waapLog << LogField("httpSourceId", m_source_identifier); + waapLog << LogField("sourcePort", m_remote_port); + waapLog << LogField("httpHostName", m_hostStr); + waapLog << LogField("httpMethod", m_methodStr); + const auto& autonomousSecurityDecision = std::dynamic_pointer_cast( + m_waapDecision.getDecision(AUTONOMOUS_SECURITY_DECISION)); + bool send_extended_log = shouldSendExtendedLog(triggerLog); + if (send_extended_log || triggerLog->webUrlPath || autonomousSecurityDecision->getOverridesLog()) { + std::string httpUriPath = m_uriPath; + + if (httpUriPath.length() > MAX_LOG_FIELD_SIZE) + { + httpUriPath.resize(MAX_LOG_FIELD_SIZE); + } + + waapLog << LogField("httpUriPath", httpUriPath, LogFieldOption::XORANDB64); + } + if (send_extended_log || triggerLog->webUrlQuery || autonomousSecurityDecision->getOverridesLog()) { + std::string uriQuery = m_uriQuery; + if (uriQuery.length() > MAX_LOG_FIELD_SIZE) + { + uriQuery.resize(MAX_LOG_FIELD_SIZE); + } + waapLog << LogField("httpUriQuery", uriQuery, LogFieldOption::XORANDB64); + } + if (send_extended_log || triggerLog->webHeaders || autonomousSecurityDecision->getOverridesLog()) { + waapLog << LogField("httpRequestHeaders", logHeadersStr(), LogFieldOption::XORANDB64); + } + // Log http response code if it is known + if (m_responseStatus != 0 && send_extended_log && triggerLog->responseCode) { + waapLog << LogField("httpResponseCode", std::to_string(m_responseStatus)); + } + + // Count of bytes available to send to the log + std::string requestBodyToLog = (send_extended_log || triggerLog->webBody) ? + m_request_body : std::string(); + std::string responseBodyToLog = m_response_body; + if (!shouldBlock && responseBodyToLog.empty()) + { + responseBodyToLog = ""; + } + + if (!requestBodyToLog.empty()) { + size_t requestBodyMaxSize = MAX_LOG_FIELD_SIZE - std::min(MIN_RESP_BODY_LOG_FIELD_SIZE, + responseBodyToLog.size()); + // Limit request body log field size + if (requestBodyToLog.length() > requestBodyMaxSize) + { + requestBodyToLog.resize(requestBodyMaxSize); + } + } + + if (!m_response_body.empty()) { + size_t responseBodyMaxSize = MAX_LOG_FIELD_SIZE - requestBodyToLog.size(); + // Limit response body log field size + if (responseBodyToLog.length() > responseBodyMaxSize) + { + responseBodyToLog.resize(responseBodyMaxSize); + } + } + + if (!requestBodyToLog.empty()) + { + waapLog << LogField("httpRequestBody", requestBodyToLog, LogFieldOption::XORANDB64); + } + + if (!responseBodyToLog.empty() && send_extended_log && triggerLog->responseBody) + { + waapLog << LogField("httpResponseBody", responseBodyToLog, LogFieldOption::XORANDB64); + } + + waapLog << LogField("ruleId", m_siteConfig->get_RuleId()); + waapLog << LogField("securityAction", shouldBlock ? "Prevent" : "Detect"); + waapLog << LogField("waapOverride", logOverride); + waapLog << LogField("practiceType", "Threat Prevention"); + waapLog << LogField("practiceSubType", m_siteConfig->get_PracticeSubType()); + waapLog << LogField("ruleName", m_siteConfig->get_RuleName()); + waapLog << LogField("practiceId", m_siteConfig->get_PracticeId()); + waapLog << LogField("practiceName", m_siteConfig->get_PracticeName()); + waapLog << LogField("waapIncidentType", incidentType); + + // Registering this value would append the list of matched override IDs to the unified log + if (!m_matchedOverrideIds.empty()) { + // Convert set to vector and send to log as a list + std::vector vOverrideIds(m_matchedOverrideIds.size()); + std::copy(m_matchedOverrideIds.begin(), m_matchedOverrideIds.end(), vOverrideIds.begin()); + waapLog.addToOrigin(LogField("exceptionIdList", vOverrideIds)); + } +} + +void +Waf2Transaction::sendLog() +{ + dbgFlow(D_WAAP); + m_waapDecision.orderDecisions(); + if (m_siteConfig == NULL) { + dbgWarning(D_WAAP) << + "Waf2Transaction::sendLog: no site policy associated with transaction - not sending a log"; + return; + } + std::string attackTypes = buildAttackTypes(); + std::string logOverride = "None"; + DecisionTelemetryData telemetryData; + std::string assetId = m_siteConfig->get_AssetId(); + const auto& autonomousSecurityDecision = std::dynamic_pointer_cast( + m_waapDecision.getDecision(AUTONOMOUS_SECURITY_DECISION)); + + telemetryData.source = getSourceIdentifier(); + telemetryData.assetName = m_siteConfig->get_AssetName(); + telemetryData.practiceId = m_siteConfig->get_PracticeId(); + telemetryData.practiceName = m_siteConfig->get_PracticeName(); + if (m_scanResult) { + telemetryData.attackTypes = m_scanResult->attack_types; + } + telemetryData.threat = autonomousSecurityDecision->getThreatLevel(); + if (m_overrideState.bForceBlock) { + telemetryData.blockType = FORCE_BLOCK; + } + else if (m_overrideState.bForceException) { + telemetryData.blockType = FORCE_EXCEPTION; + } + else if (m_waapDecision.getDecision(USER_LIMITS_DECISION)->shouldBlock()) { + telemetryData.blockType = LIMIT_BLOCK; + } + else if (autonomousSecurityDecision->shouldBlock()) { + telemetryData.blockType = WAF_BLOCK; + } + else if (m_waapDecision.getDecision(CSRF_DECISION)->shouldBlock()) { + telemetryData.blockType = CSRF_BLOCK; + } + else { + telemetryData.blockType = NOT_BLOCKING; + } + + WaapTelemetryEvent(assetId, telemetryData).notify(); + + if (m_overrideState.bIgnoreLog) { + dbgTrace(D_WAAP) << "Waf2Transaction::sendLog: override is to ignore log - not sending a log"; + return; + } + + bool shouldBlock = false; + if (m_overrideState.bForceBlock) { + // If override forces "reject" decision, mention it in the "override" log field. + logOverride = OVERRIDE_DROP; + shouldBlock = true; + } + else if (m_overrideState.bForceException) { + // If override forces "allow" decision, mention it in the "override" log field. + logOverride = OVERRIDE_ACCEPT; + } else if (m_scanner.getIgnoreOverride()) { + logOverride = OVERRIDE_IGNORE; + } + + // Get triggers + const std::shared_ptr triggerPolicy = m_siteConfig->get_TriggerPolicy(); + + if (!triggerPolicy || triggerPolicy->triggers.empty()) { + dbgTrace(D_WAAP) << "Waf2Transaction::sendLog: found no triggers (or triggers are absent) - not sending a log"; + return; + } + + const std::shared_ptr triggerLog = getTriggerLog(triggerPolicy); + + // If there were no triggers of type Log - do not send log + if (!triggerLog) { + dbgTrace(D_WAAP) << "Waf2Transaction::sendLog: found no triggers of type 'Log' - not sending a log"; + return; + } + + static int cur_grace_logs = 0; + bool grace_period = is_hybrid_mode && cur_grace_logs < max_grace_logs; + bool send_extended_log = grace_period || shouldSendExtendedLog(triggerLog); + if (grace_period) { + dbgTrace(D_WAAP) + << "Waf2Transaction::sendLog: current grace log index: " + << cur_grace_logs + 1 + << " out of " + << max_grace_logs; + } + + shouldBlock |= m_waapDecision.getShouldBlockFromHighestPriorityDecision(); + // Do not send Detect log if trigger disallows it + if (!send_extended_log && shouldBlock == false && !triggerLog->tpDetect && + !autonomousSecurityDecision->getOverridesLog()) + { + dbgTrace(D_WAAP) << "Waf2Transaction::sendLog: not sending Detect log (triggers)"; + return; + } + + // Do not send Prevent log if trigger disallows it + if (!send_extended_log && shouldBlock == true && !triggerLog->tpPrevent && + !autonomousSecurityDecision->getOverridesLog()) + { + dbgTrace(D_WAAP) << "Waf2Transaction::sendLog: not sending Prevent log (triggers)"; + return; + } + + // In case no decision to block or log - send log if extend log or override + if (!m_waapDecision.anyDecisionsToLogOrBlock()) + { + if (send_extended_log || autonomousSecurityDecision->getOverridesLog()) + { + sendAutonomousSecurityLog(triggerLog, shouldBlock, logOverride, attackTypes); + dbgTrace(D_WAAP) << "Waf2Transaction::sendLog()::" << + "sending autonomous security log due to either extended log or an override"; + } + else + { + dbgTrace(D_WAAP) << "Waf2Transaction::sendLog: no decision to log"; + } + return; + } + + DecisionType decision_type = m_waapDecision.getHighestPriorityDecisionToLog(); + if (decision_type == DecisionType::NO_WAAP_DECISION) { + if (send_extended_log || autonomousSecurityDecision->getOverridesLog()) { + sendAutonomousSecurityLog(triggerLog, shouldBlock, logOverride, attackTypes); + if (grace_period) { + dbgTrace(D_WAAP) + << "Waf2Transaction::sendLog: Sending log in grace period. Log " + << ++cur_grace_logs + << "out of " + << max_grace_logs; + } + } + dbgTrace(D_WAAP) << "Waf2Transaction::sendLog: decisions marked for block only"; + return; + } + + auto maybeLogTriggerConf = getConfiguration("rulebase", "log"); + switch (decision_type) + { + case USER_LIMITS_DECISION: { + std::string incidentDetails; + std::string incidentType; + if (isIllegalMethodViolation()) { + incidentDetails += "Http method received: "; + incidentDetails += getMethod(); + incidentType += "Illegal http method violation"; + } + else { + auto strData = getViolatedUserLimitStrData(); + incidentDetails += "Http request "; + incidentDetails += strData.type; + incidentDetails += " ("; + incidentDetails += strData.policy; + incidentDetails += ")"; + incidentType += "Http limit violation"; + } + + LogGenWrapper logGenWrapper( + maybeLogTriggerConf, + "Web Request", + ReportIS::Audience::SECURITY, + LogTriggerConf::SecurityType::ThreatPrevention, + Severity::HIGH, + Priority::HIGH, + shouldBlock); + + LogGen& waap_log = logGenWrapper.getLogGen(); + appendCommonLogFields(waap_log, triggerLog, shouldBlock, logOverride, incidentType); + waap_log << LogField("waapIncidentDetails", incidentDetails); + waap_log << LogField("eventConfidence", "High"); + if (grace_period) { + dbgTrace(D_WAAP) + << "Waf2Transaction::sendLog: Sending log in grace period. Log " + << ++cur_grace_logs + << "out of " + << max_grace_logs; + } + break; + } + case OPEN_REDIRECT_DECISION: + case ERROR_LIMITING_DECISION: + case RATE_LIMITING_DECISION: + case ERROR_DISCLOSURE_DECISION: { + LogGenWrapper logGenWrapper( + maybeLogTriggerConf, + "API Request", + ReportIS::Audience::SECURITY, + LogTriggerConf::SecurityType::ThreatPrevention, + Severity::CRITICAL, + Priority::HIGH, + shouldBlock); + + LogGen& waap_log = logGenWrapper.getLogGen(); + waap_log << LogField("eventConfidence", "Very High"); + + std::string incidentDetails; + std::string incidentType; + m_waapDecision.getIncidentLogFields( + std::to_string(m_responseStatus), + incidentDetails, + incidentType + ); + + if (decision_type == ERROR_DISCLOSURE_DECISION) { + waap_log << LogField("waapFoundIndicators", getKeywordMatchesStr(), LogFieldOption::XORANDB64); + } + + appendCommonLogFields(waap_log, triggerLog, shouldBlock, logOverride, incidentType); + + waap_log << LogField("waapIncidentDetails", incidentDetails); + if (grace_period) { + dbgTrace(D_WAAP) + << "Waf2Transaction::sendLog: Sending log in grace period. Log " + << ++cur_grace_logs + << "out of " + << max_grace_logs; + } + break; + } + case CSRF_DECISION: { + LogGenWrapper logGenWrapper( + maybeLogTriggerConf, + "CSRF Protection", + ReportIS::Audience::SECURITY, + LogTriggerConf::SecurityType::ThreatPrevention, + Severity::CRITICAL, + Priority::HIGH, + shouldBlock); + + LogGen& waap_log = logGenWrapper.getLogGen(); + appendCommonLogFields(waap_log, triggerLog, shouldBlock, logOverride, "Cross Site Request Forgery"); + waap_log << LogField("waapIncidentDetails", "CSRF Attack discovered."); + if (grace_period) { + dbgTrace(D_WAAP) + << "Waf2Transaction::sendLog: Sending log in grace period. Log " + << ++cur_grace_logs + << "out of " + << max_grace_logs; + } + break; + } + case AUTONOMOUS_SECURITY_DECISION: { + if (triggerLog->webRequests || + send_extended_log || + autonomousSecurityDecision->getThreatLevel() != ThreatLevel::NO_THREAT || + autonomousSecurityDecision->getOverridesLog()) { + sendAutonomousSecurityLog(triggerLog, shouldBlock, logOverride, attackTypes); + if (grace_period) { + dbgTrace(D_WAAP) + << "Waf2Transaction::sendLog: Sending log in grace period. Log " + << ++cur_grace_logs + << "out of " + << max_grace_logs; + } + } + break; + } + default: + static_assert(true, "Illegal DecisionType enum value"); + break; + } // end switch +} + +bool +Waf2Transaction::decideAutonomousSecurity( + const IWaapConfig &sitePolicy, + int mode, + bool afterHeaders, + AnalysisResult &transactionResult, + const std::string &poolName, + PolicyCounterType fpClassification) +{ + dbgFlow(D_WAAP) << + "Waf2Transaction::decideAutonomousSecurity(): " << + "mode=" << mode << + ", afterHeaders=" << afterHeaders << + ", poolName='" << poolName << "'"; + + if (mode == 2) + { + return isSuspicious(); + } + + if (!sitePolicy.get_WebAttackMitigation()) { + // Web security not enabled + dbgTrace(D_WAAP) << "Autonomous security is not enabled in policy."; + return false; + } + + std::shared_ptr decision = std::dynamic_pointer_cast( + m_waapDecision.getDecision(AUTONOMOUS_SECURITY_DECISION)); + + // Do not call stage2 so it doesn't learn from exceptions. + // Also do not call stage2 for attacks found in parameter name + if (!m_overrideState.bForceException && !(m_scanResult && m_scanResult->m_isAttackInParam)) { + if (!m_processedUri) { + dbgWarning(D_WAAP) << "decideAutonomousSecurity(): processing URI although is was supposed " + "to be processed earlier ..."; + processUri(m_uriStr.c_str(), "url"); + } + + if (!m_processedHeaders) { + dbgWarning(D_WAAP) << "decideAutonomousSecurity(): processing Headers although is was supposed " + "to be processed earlier ..."; + scanHeaders(); + } + + dbgTrace(D_WAAP) << "decideAutonomousSecurity(): processing stage2 for final decision ..."; + + // Call stage2 + transactionResult = + Singleton::Consume::by()->analyzeData(this, &sitePolicy); + + decision->setThreatLevel(transactionResult.threatLevel); + + decision->setBlock(transactionResult.shouldBlock); + + // Once these are known - fill the values to be included in the log + decision->setRelativeReputation(transactionResult.d2Analysis.relativeReputation); + decision->setFpMitigationScore(transactionResult.d2Analysis.fpMitigationScore); + decision->setFinalScore(transactionResult.d2Analysis.finalScore); + decision->setRelativeReputationMean(transactionResult.d2Analysis.reputationMean); + decision->setVariance(transactionResult.d2Analysis.variance); + + dbgTrace(D_WAAP) << "decideAutonomousSecurity(): stage2 decision is: " << + decision->shouldBlock() << "; threatLevel: " << decision->getThreatLevel() << + "; blockingLevel: " << static_cast::type>( + sitePolicy.get_BlockingLevel()); + + if (!afterHeaders || decision->shouldBlock()) { + ScoreBuilderData sbData; + + sbData.m_fpClassification = transactionResult.d2Analysis.fpClassification; + sbData.m_sourceIdentifier = getSourceIdentifier(); + sbData.m_keywordsCombinations = getKeywordsCombinations(); + sbData.m_keywordsMatches = getKeywordMatches(); + sbData.m_userAgent = getUserAgent(); + sbData.m_sample = getSample(); + sbData.m_relativeReputation = transactionResult.d2Analysis.relativeReputation; + + if (fpClassification != UNKNOWN_TYPE) { + sbData.m_fpClassification = fpClassification; + } + + learnScore(sbData, poolName); + } + } + + // Fill attack details for attacks found in parameter names + if (!m_overrideState.bForceException && m_scanResult && m_scanResult->m_isAttackInParam) { + // Since stage2 learning doesn't run in this case, assume stage1 score is the final score + float finalScore = m_scanResult->score; + ThreatLevel threat = Waap::Conversions::convertFinalScoreToThreatLevel(finalScore); + bool shouldBlock = Waap::Conversions::shouldDoWafBlocking(&sitePolicy, threat); + + dbgTrace(D_WAAP) << "attack_in_param without stage2 analysis: final score: " << finalScore << + ", threat level: " << threat << "\nWAF2 decision to block: " << + (shouldBlock ? "block" : "pass"); + + decision->setFinalScore(finalScore); + decision->setThreatLevel(threat); + decision->setBlock(shouldBlock); + + // Fill transactionResult + transactionResult.d2Analysis.finalScore = finalScore; + transactionResult.shouldBlock = shouldBlock; + transactionResult.threatLevel = threat; + } + + // Apply overrides + if (m_overrideState.bForceBlock) { + dbgTrace(D_WAAP) << "decideAutonomousSecurity(): decision was " << decision->shouldBlock() << + " and override forces REJECT ..."; + decision->setBlock(true); + if (!m_overrideState.bIgnoreLog) + { + decision->setOverridesLog(true); + } + } + else if (m_overrideState.bForceException) { + dbgTrace(D_WAAP) << "decideAutonomousSecurity(): decision was " << decision->shouldBlock() << + " and override forces ALLOW ..."; + decision->setBlock(false); + if (!m_overrideState.bIgnoreLog) + { + decision->setOverridesLog(true); + } + } + + if(decision->getThreatLevel() <= ThreatLevel::THREAT_INFO) { + decision->setLog(false); + } else { + decision->setLog(true); + } + + return decision->shouldBlock(); +} + +void Waf2Transaction::handleCsrfHeaderInjection(std::string& injectStr) +{ + m_csrfState.injectCookieHeader(injectStr); +} + +// Disables response injection (masking any pending injection reasons such as from antibot or csrf) +void +Waf2Transaction::clearAllInjectionReasons() { + m_responseInjectReasons.clear(); +} + +// Returns true if WAAP engine is interested in receiving more information about response for this transaction +bool Waf2Transaction::shouldInspectResponse() +{ + return m_responseInspectReasons.shouldInspect() || m_responseInjectReasons.shouldInject(); +} +bool Waf2Transaction::shouldInjectResponse() +{ + return m_responseInjectReasons.shouldInject(); +} + +bool Waf2Transaction::decideResponse() +{ + dbgTrace(D_WAAP) << "Waf2Transaction::decideResponse()"; + + if(m_waapDecision.getDecision(ERROR_LIMITING_DECISION)->shouldBlock()) { + return false; // block + } + if(m_waapDecision.getDecision(RATE_LIMITING_DECISION)->shouldBlock()) { + return false; // block + } + + bool openRedirectBlock = m_waapDecision.getDecision(OPEN_REDIRECT_DECISION)->shouldBlock(); + bool errorDisclosureBlock = m_waapDecision.getDecision(ERROR_DISCLOSURE_DECISION)->shouldBlock(); + if (openRedirectBlock || errorDisclosureBlock) { + dbgTrace(D_WAAP) << "Waf2Transaction::decideResponse(): blocking due to" << + " OpenRedirect:" << openRedirectBlock << + " ErrorDisclosure:" << errorDisclosureBlock; + return false; // block + } + + if (m_siteConfig) { + const std::shared_ptr triggerPolicy = m_siteConfig->get_TriggerPolicy(); + if (!triggerPolicy) { + dbgTrace(D_WAAP) << "Trigger policy was not found. Returning true (accept)"; + return true; // accept + } + + const std::shared_ptr triggerLog = getTriggerLog(triggerPolicy); + if (!triggerLog) { + dbgTrace(D_WAAP) << "Log trigger configuration was not found. Returning true (accept)"; + return true; // accept + } + + auto env = Singleton::Consume::by(); + auto http_chunk_type = env->get("HTTP Chunk type"); + bool should_send_extended_log = shouldSendExtendedLog(triggerLog) && http_chunk_type.ok(); + if (should_send_extended_log && + *http_chunk_type == ngx_http_chunk_type_e::RESPONSE_CODE && + !triggerLog->responseBody + ) { + should_send_extended_log = false; + } else if (should_send_extended_log && + *http_chunk_type == ngx_http_chunk_type_e::REQUEST_END && + !triggerLog->responseCode && + !triggerLog->responseBody + ) { + should_send_extended_log = false; + } + + dbgTrace(D_WAAP) + << "Setting flag for collection of respond content logging to: " + << (should_send_extended_log ? "True": "False"); + m_responseInspectReasons.setCollectResponseForLog(should_send_extended_log); + + } + + dbgTrace(D_WAAP) << "Waf2Transaction::decideResponse: returns true (accept)"; + return true; // accept +} + +bool +Waf2Transaction::reportScanResult(const Waf2ScanResult &res) { + if (get_ignoreScore() || (res.score >= SCORE_THRESHOLD && + (m_scanResult == nullptr || res.score > m_scanResult->score))) + { + // Forget any previous scan result and replace with new + delete m_scanResult; + m_scanResult = new Waf2ScanResult(res); + return true; + } + + return false; +} + +bool +Waf2Transaction::shouldIgnoreOverride(const Waf2ScanResult &res) { + dbgTrace(D_WAAP) << "reading exceptions"; + + auto exceptions = getConfiguration("rulebase", "exception"); + if (!exceptions.ok()) return false; + + dbgTrace(D_WAAP) << "matching exceptions"; + + std::unordered_map> exceptions_dict; + + if (res.location != "referer") { + // collect param name + exceptions_dict["paramName"].insert(res.param_name); + exceptions_dict["paramName"].insert(IndicatorsFiltersManager::generateKey(res.location, res.param_name, this)); + + std::set param_name_set; + param_name_set.insert(res.param_name); + param_name_set.insert(IndicatorsFiltersManager::generateKey(res.location, res.param_name, this)); + + // collect param value + exceptions_dict["paramValue"].insert(res.unescaped_line); + + ScopedContext ctx; + ctx.registerValue("paramValue", res.unescaped_line); + ctx.registerValue>("paramName", param_name_set); + + // collect sourceip, sourceIdentifier, url + exceptions_dict["sourceIP"].insert(m_remote_addr); + exceptions_dict["sourceIdentifier"].insert(m_source_identifier); + exceptions_dict["url"].insert(getUriStr()); + exceptions_dict["hostName"].insert(m_hostStr); + + // calling behavior and check if there is a behavior that match to this specific param name. + auto behaviors = exceptions.unpack().getBehavior(exceptions_dict); + for (auto const &behavior : behaviors) { + if (behavior == action_ignore) { + dbgTrace(D_WAAP) << "matched exceptions for " << res.param_name << " should ignore."; + std::string overrideId = behavior.getId(); + if (!overrideId.empty()) { + m_matchedOverrideIds.insert(overrideId); + } + return true; + } + } + } + + return false; +} + +const std::string Waf2Transaction::buildAttackTypes() const +{ + typedef std::map>::const_iterator attack_types_iter; + if (m_scanResult) + { + for (const std::string ®ex_name : m_found_patterns) + { + attack_types_iter attack_types_for_regex = + m_pWaapAssetState->getSignatures()->m_attack_types.find(regex_name); + if (attack_types_for_regex != m_pWaapAssetState->getSignatures()->m_attack_types.end()) + { + for (const std::string &attack_type : attack_types_for_regex->second) + { + m_scanResult->attack_types.insert(attack_type); + } + } + else {m_scanResult->attack_types.insert("General");} + } + + if (Waap::Util::vectorStringContain(m_scanResult->keyword_matches, "xml_entity")) { + m_scanResult->attack_types.insert("XML External Entity"); + } + + if (Waap::Util::vectorStringContain(m_scanResult->keyword_matches, "url_instead_of_file")) { + m_scanResult->attack_types.insert("URL instead of file"); + } + + auto csrfDecision = m_waapDecision.getDecision(CSRF_DECISION); + if(csrfDecision && csrfDecision->shouldBlock()) { + m_scanResult->attack_types.insert("Cross Site Request Forgery"); + } + auto openRedirectDecision = m_waapDecision.getDecision(OPEN_REDIRECT_DECISION); + if (openRedirectDecision && openRedirectDecision->shouldBlock()) { + m_scanResult->attack_types.insert("Open Redirect"); + } + + if (m_scanResult->attack_types.find("General") != m_scanResult->attack_types.end() + && m_scanResult->attack_types.size() > 1) { + m_scanResult->attack_types.erase("General"); + } + return Waap::Util::setToString(m_scanResult->attack_types, false); + } + + return ""; +} + +void Waf2Transaction::collectFoundPatterns() +{ + if (m_scanResult) + { + for (const std::pair> &found_pattern : m_scanResult->found_patterns) + { + const std::string ®ex_name = found_pattern.first; // the regex name (key) + m_found_patterns.insert(regex_name); + } + } +} + +bool Waf2Transaction::shouldSendExtendedLog(const std::shared_ptr &trigger_log) const +{ + if (!trigger_log->extendLogging) + { + dbgTrace(D_WAAP) << "Should not send extended log. Extended log is disabled."; + return false; + } + + auto autonomousSecurityDecision = std::dynamic_pointer_cast( + m_waapDecision.getDecision(AUTONOMOUS_SECURITY_DECISION)); + ReportIS::Severity severity = Waap::Util::computeSeverityFromThreatLevel( + autonomousSecurityDecision->getThreatLevel()); + + if (trigger_log->extendLoggingMinSeverity == "Critical") + { + if (severity == ReportIS::Severity::CRITICAL) + { + dbgTrace(D_WAAP) << "Should send extended logging. Min Severity Critical. Severity: " << (int) severity; + return true; + } + dbgTrace(D_WAAP) << "Should not send extended logging. Min Severity Critical. Severity: " << (int) severity; + return false; + } + else if (trigger_log->extendLoggingMinSeverity == "High") + { + if (severity == ReportIS::Severity::CRITICAL || severity == ReportIS::Severity::HIGH) + { + dbgTrace(D_WAAP) << "Should send extended logging. Min Severity High. Severity: " << (int) severity; + return true; + } + dbgTrace(D_WAAP) << "Should not send extended logging. Min Severity High. Severity: " << (int) severity; + return false; + } + + dbgTrace(D_WAAP) << "Should not send extended logging. Min Severity: " << trigger_log->extendLoggingMinSeverity; + return false; +} diff --git a/components/security_apps/waap/waap_clib/Waf2Engine.h b/components/security_apps/waap/waap_clib/Waf2Engine.h new file mode 100755 index 0000000..cc3e16a --- /dev/null +++ b/components/security_apps/waap/waap_clib/Waf2Engine.h @@ -0,0 +1,352 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __WAF2_TRANSACTION_H__99e4201a +#define __WAF2_TRANSACTION_H__99e4201a + +#include "Csrf.h" +#include "UserLimitsPolicy.h" +#include "ParserBase.h" +#include "DeepParser.h" +#include "WaapAssetState.h" +#include "PatternMatcher.h" +#include "Waf2Util.h" +#include "WaapConfigApplication.h" +#include "WaapConfigApi.h" +#include "WaapDecision.h" +#include "DeepAnalyzer.h" +#include +#include +#include +#include +#include +#include +#include // uuid class +#include // uuid generators +#include +#include +#include "i_transaction.h" +#include "i_waap_telemetry.h" +#include "i_deepAnalyzer.h" +#include "table_opaque.h" +#include "WaapResponseInspectReasons.h" +#include "WaapResponseInjectReasons.h" +#include "WaapOpenRedirect.h" +#include "WaapOpenRedirectPolicy.h" +#include "WaapScanner.h" +#include "singleton.h" + +struct DecisionTelemetryData; +class Waf2Transaction; + +// Callback that is called upon completion of next sub transaction +typedef void(*subtransaction_cb_t)(Waf2Transaction* subTransaction, void *ctx); +#define OVERRIDE_ACCEPT "Accept" +#define OVERRIDE_DROP "Drop" + +class Waf2Transaction : + public IWaf2Transaction, + public TableOpaqueSerialize, + public Singleton::Consume, + private boost::noncopyable, + Singleton::Consume, + Singleton::Consume +{ +public: + Waf2Transaction(std::shared_ptr pWaapAssetState); + Waf2Transaction(); + ~Waf2Transaction(); + + // setters + void set_transaction_time(const char *log_time); + void set_transaction_remote(const char *remote_addr, int remote_port); + void set_transaction_local(const char *local_addr, int local_port); + void set_method(const char *method); + void set_uri(const char *uri); + void set_host(const char *host); + + // getters + const std::string& getRemoteAddr() const; + virtual const std::string getUri() const; + const std::string getUriStr() const; + const std::string& getSourceIdentifier() const; + virtual const std::string getUserAgent() const; + const std::string getParam() const; + const std::string getParamKey() const; + const std::vector getKeywordMatches() const; + const std::vector getFilteredKeywords() const; + const std::map> getFilteredVerbose() const; + virtual const std::vector getKeywordsCombinations() const; + const std::vector& getKeywordInfo() const; + const std::vector >& getKvPairs() const; + const std::string getKeywordMatchesStr() const; + const std::string getFilteredKeywordsStr() const; + const std::string getSample() const; + const std::string getLastScanSample() const; + virtual const std::string& getLastScanParamName() const; + double getScore() const; + const std::vector getScoreArray() const; + Waap::CSRF::State& getCsrfState(); + const std::set getFoundPatterns() const; + const std::string getContentTypeStr() const; + Waap::Util::ContentType getContentType() const; + int getRemotePort() const; + const std::string getLocalAddress() const; + int getLocalPort() const; + const std::string getLogTime() const; + ParserBase* getRequestBodyParser(); + const std::string getMethod() const; + const std::string getHost() const; + const std::string getCookie() const; + const std::vector getNotes() const; + DeepParser& getDeepParser(); + std::vector > getHdrPairs() const; + virtual const std::string getHdrContent(std::string hdrName) const; + const std::string getRequestBody() const; + const std::string getTransactionIdStr() const; + const WaapDecision &getWaapDecision() const; + virtual std::shared_ptr getAssetState(); + virtual const std::string getLocation() const; + + ngx_http_cp_verdict_e getUserLimitVerdict(); + const std::string getUserLimitVerdictStr() const; + const std::string getViolatedUserLimitTypeStr() const; + + virtual HeaderType detectHeaderType(const char* name, int name_len); + HeaderType checkCleanHeader(const char* name, int name_len, const char* value, int value_len) const; + + // flow control + void start(); + + void start_request_hdrs(); + void add_request_hdr(const char *name, int name_len, const char *value, int value_len); + void end_request_hdrs(); + void start_request_body(); + void add_request_body_chunk(const char *data, int data_len); + void end_request_body(); + void end_request(); + + void start_response(int response_status, int http_version); + void start_response_hdrs(); + void add_response_hdr(const char* name, int name_len, const char* value, int value_len); + void end_response_hdrs(); + void start_response_body(); + void add_response_body_chunk(const char* data, int data_len); + void end_response_body(); + void end_response(); + void extractEnvSourceIdentifier(); + void finish(); + Waf2TransactionFlags &getTransactionFlags(); + + // inject function + void checkShouldInject(); + void completeInjectionResponseBody(std::string& strInjection); + bool findHtmlTagToInject(const char* data, int data_len, int& pos); + bool isHtmlType(const char* data, int data_len); + + // decision functions + void set_ignoreScore(bool ignoreScore); + bool get_ignoreScore() const { return m_ignoreScore; } + void decide( + bool& bForceBlock, + bool& bForceException, + int mode); + bool decideAfterHeaders(); + int decideFinal( + int mode, + AnalysisResult &transactionResult, + const std::string &poolName = KEYWORDS_SCORE_POOL_BASE, + PolicyCounterType fpClassification = UNKNOWN_TYPE); + bool decideAutonomousSecurity( + const IWaapConfig& config, + int mode, + bool afterHeaders, + AnalysisResult &transactionResult, + const std::string &poolName, + PolicyCounterType fpClassification = UNKNOWN_TYPE); + bool decideResponse(); + void clearAllInjectionReasons(); + bool shouldInspectResponse(); + bool shouldInjectResponse(); + bool shouldInjectCSRF(); + bool shouldInjectSecurityHeaders(); + void handleCsrfHeaderInjection(std::string& injectStr); + void handleSecurityHeadersInjection(std::vector>& injectHeaderStrs); + void disableShouldInjectSecurityHeaders(); + + bool shouldSendExtendedLog(const std::shared_ptr &trigger_log) const; + + // query + virtual bool isSuspicious() const; + virtual uint64_t getIndex() const; + virtual void setIndex(uint64_t index); + + //misc + void sendLog(); + const std::string logHeadersStr() const; + void learnScore(ScoreBuilderData& data, const std::string &poolName); + const std::string buildAttackTypes() const; + void collectFoundPatterns(); + ReportIS::Severity computeEventSeverityFromDecision() const; + + // LCOV_EXCL_START - sync functions, can only be tested once the sync module exists + + static std::string name() { return "Waf2Transaction"; }; + static std::unique_ptr prototype() { return std::make_unique(); }; + static uint currVer() { return 0; } + static uint minVer() { return 0; } + + template + void serialize(T& ar, uint) { + ar(0); + } + + // LCOV_EXCL_STOP + + bool reportScanResult(const Waf2ScanResult &res); + bool shouldIgnoreOverride(const Waf2ScanResult &res); + Waap::OpenRedirect::State &getOpenRedirectState() { return m_openRedirectState; } + IWaapConfig* getSiteConfig() { return m_siteConfig; } + void addNote(const std::string ¬e) { m_notes.push_back(note); } + +private: + int finalizeDecision(IWaapConfig *sitePolicy, bool shouldBlock); + const std::shared_ptr getTriggerLog(const std::shared_ptr& + triggerPolicy) const; + void sendAutonomousSecurityLog( + const std::shared_ptr& triggerLog, + bool shouldBlock, + const std::string& logOverride, + const std::string& attackTypes) const; + void appendCommonLogFields(LogGen& waapLog, + const std::shared_ptr &triggerLog, + bool shouldBlock, + const std::string& logOverride, + const std::string& incidentType) const; + std::string getUserReputationStr(double relativeReputation) const; + bool isTrustedSource() const; + + void setCurrentAssetState(IWaapConfig* sitePolicy); + bool setCurrentAssetContext(); + bool checkIsScanningRequired(); + Waap::Override::State getOverrideState(IWaapConfig* sitePolicy); + + // User limits functions + void createUserLimitsState(); + bool isUrlLimitReached(size_t size); + bool isHttpHeaderLimitReached(const std::string& name, const std::string& value); + bool isHttpBodyLimitReached(size_t chunkSize); + bool isObjectDepthLimitReached(size_t depth); + bool isPreventModeValidMethod(const std::string& method); + bool isUserLimitReached() const; + bool isIllegalMethodViolation() const; + const Waap::UserLimits::ViolatedStrData& getViolatedUserLimitStrData() const; + size_t getViolatingUserLimitSize() const; + + // Internal + void processUri(const char *uri, const std::string &scanStage); + void parseContentType(const char* value, int value_len); + void parseCookie(const char* value, int value_len); + void parseReferer(const char* value, int value_len); + void parseUnknownHeaderName(const char* name, int name_len); + void parseGenericHeaderValue(const std::string &headerName, const char* value, int value_len); + void scanSpecificHeder(const char* name, int name_len, const char* value, int value_len); + void detectSpecificHeader(const char* name, int name_len, const char* value, int value_len); + void detectHeaders(); + void scanHeaders(); + void clearRequestParserState(); + void scanErrDisclosureBuffer(); + + std::shared_ptr m_pWaapAssetState; + bool m_ignoreScore; // override the scoring filter and (effectively) take the last suspicious parameter, + // instead of the one with highest score that is > SCORE_THRESHOLD + boost::uuids::uuid m_transaction_id; + std::string m_log_time; + std::string m_remote_addr; + std::string m_source_identifier; + int m_remote_port; + std::string m_local_addr; + int m_local_port; + + // Matched override IDs + std::set m_matchedOverrideIds; + + //csrf state + Waap::CSRF::State m_csrfState; + // UserLimits state + std::shared_ptr m_userLimitsState; + + WaapConfigAPI m_ngenAPIConfig; + WaapConfigApplication m_ngenSiteConfig; + IWaapConfig* m_siteConfig; + + // Current content type and (for multiplart), MIME boundary identifier + Waap::Util::ContentType m_contentType; + + // Request body parser, type is derived from headers/ContentType. + // May be NULL if request payload is of unknown type! + ParserBase *m_requestBodyParser; + + // find html tag + char m_tagHist[6]; // strlen("") + size_t m_tagHistPos; + bool m_isUrlValid; + + Waap::Scanner m_scanner; // Receives the param+value pairs from DeepParser and scans them + DeepParser m_deepParser; // recursive (deep) parser that can parse deep content encodings + // hierarchies like XML in JSON in URLEncode in ... + BufferedReceiver m_deepParserReceiver; // buffered receiver forwarding to m_deepParser + Waf2ScanResult *m_scanResult; + + std::string m_methodStr; + std::string m_uriStr; + std::string m_uriPath; + std::string m_uriReferer; + std::string m_uriQuery; + std::string m_contentTypeStr; + std::string m_hostStr; + std::string m_userAgentStr; + std::string m_cookieStr; + std::vector m_notes; + std::set m_found_patterns; + + Waap::OpenRedirect::State m_openRedirectState; + std::map hdrs_map; + std::string m_request_body; + std::string m_response_body; + std::string m_response_body_err_disclosure; + size_t m_request_body_bytes_received; + size_t m_response_body_bytes_received; + + bool m_processedUri; + bool m_processedHeaders; + bool m_isScanningRequired; + int m_responseStatus; + Waap::ResponseInspectReasons m_responseInspectReasons; + Waap::ResponseInjectReasons m_responseInjectReasons; + WaapDecision m_waapDecision; + Waap::Override::State m_overrideState; + + uint64_t m_index; + + // Cached pointer to const triggerLog (hence mutable) + mutable std::shared_ptr m_triggerLog; + + Waf2TransactionFlags m_waf2TransactionFlags; + + // Grace period for logging + int max_grace_logs; + bool is_hybrid_mode = false; +}; + +#endif // __WAF2_TRANSACTION_H__99e4201a diff --git a/components/security_apps/waap/waap_clib/Waf2EngineGetters.cc b/components/security_apps/waap/waap_clib/Waf2EngineGetters.cc new file mode 100755 index 0000000..b13400a --- /dev/null +++ b/components/security_apps/waap/waap_clib/Waf2EngineGetters.cc @@ -0,0 +1,623 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "Waf2Engine.h" +#include "WaapOverrideFunctor.h" +#include // uuid class +#include // uuid generators +#include +#include +#include "generic_rulebase/triggers_config.h" +#include "config.h" +#include "LogGenWrapper.h" +#include + +USE_DEBUG_FLAG(D_WAAP_ULIMITS); + +#define LOW_REPUTATION_THRESHOLD 4 +#define NORMAL_REPUTATION_THRESHOLD 6 +#define LOG_HEADER_MAX_LENGTH 200 + +bool Waf2Transaction::isTrustedSource() const +{ + auto policy = m_ngenSiteConfig.get_TrustedSourcesPolicy(); + if (policy == nullptr) + { + dbgTrace(D_WAAP) << "Policy for trusted sources is not set"; + return false; + } + auto trustedTypes = policy->getTrustedTypes(); + std::string cookieVal; + auto env = Singleton::Consume::by(); + auto proxy_ip = env->get(HttpTransactionData::proxy_ip_ctx); + for (auto& trustedType : trustedTypes) + { + switch (trustedType) + { + case Waap::TrustedSources::TrustedSourceType::SOURCE_IP: + dbgTrace(D_WAAP) << "check source: " << getRemoteAddr(); + return policy->isSourceTrusted(getRemoteAddr(), trustedType); + case Waap::TrustedSources::TrustedSourceType::X_FORWARDED_FOR: + if (proxy_ip.ok()) + { + return policy->isSourceTrusted(proxy_ip.unpack(), trustedType); + } else { + return false; + } + case Waap::TrustedSources::TrustedSourceType::COOKIE_OAUTH2_PROXY: + if (cookieVal.empty()) + { + cookieVal = getHdrContent("Cookie"); + } + return policy->isSourceTrusted(Waap::Util::extractKeyValueFromCookie(cookieVal, "_oauth2_proxy"), + trustedType); + default: + dbgWarning(D_WAAP) << "unrecognized trusted source identifier type: " << trustedType; + break; + } + } + return false; +} + +std::string Waf2Transaction::getUserReputationStr(double relativeReputation) const +{ + if (isTrustedSource()) { + return "Trusted"; + } + if (relativeReputation < LOW_REPUTATION_THRESHOLD) + { + return "Low"; + } + if (relativeReputation < NORMAL_REPUTATION_THRESHOLD) + { + return "Normal"; + } + return "High"; +} + +const std::string Waf2Transaction::logHeadersStr() const +{ + std::vector hdrsLog; + + for (auto hdr : hdrs_map) + { + std::string hdrName = hdr.first; + std::string hdrValue = hdr.second.substr(0, LOG_HEADER_MAX_LENGTH); + hdrsLog.push_back(hdrName + ": " + hdrValue); + } + + return Waap::Util::vecToString(hdrsLog, ';').substr(0, MAX_LOG_FIELD_SIZE); +} + + +const WaapDecision& +Waf2Transaction::getWaapDecision() const +{ + return m_waapDecision; +} +std::shared_ptr Waf2Transaction::getAssetState() +{ + return m_pWaapAssetState; +} +const std::string& Waf2Transaction::getRemoteAddr() const +{ + return m_remote_addr; +} +const std::string& Waf2Transaction::getSourceIdentifier() const +{ + return m_source_identifier; +} +const std::string Waf2Transaction::getUri() const +{ + return m_uriPath; +} +const std::string Waf2Transaction::getUriStr() const +{ + return normalize_uri(m_uriStr); +} +bool Waf2Transaction::isSuspicious() const +{ + return !!m_scanResult; +} +uint64_t Waf2Transaction::getIndex() const +{ + return m_index; +} +void Waf2Transaction::setIndex(uint64_t index) +{ + m_index = index; +} +const std::string Waf2Transaction::getUserAgent() const +{ + return m_userAgentStr; +} +const std::string Waf2Transaction::getParam() const +{ + if (m_scanResult == NULL) + { + return ""; + } + return m_scanResult->param_name; +} +const std::string Waf2Transaction::getParamKey() const +{ + if (m_scanResult == NULL) + { + return ""; + } + return IndicatorsFiltersManager::generateKey(m_scanResult->location, m_scanResult->param_name, this); +} +const std::vector Waf2Transaction::getKeywordMatches() const +{ + if (m_scanResult == NULL) + { + return std::vector(); + } + return m_scanResult->keyword_matches; +} +const std::vector Waf2Transaction::getFilteredKeywords() const +{ + if (m_scanResult == NULL) + { + return std::vector(); + } + return m_scanResult->filtered_keywords; +} +const std::map> Waf2Transaction::getFilteredVerbose() const +{ + if (m_pWaapAssetState == NULL) + { + return std::map>(); + } + return m_pWaapAssetState->getFilterVerbose(); +} +const std::vector Waf2Transaction::getKeywordsCombinations() const +{ + if (m_scanResult) + { + return m_scanResult->keywordCombinations; + } + return std::vector(); +} +const std::vector& Waf2Transaction::getKeywordInfo() const +{ + return m_deepParser.m_keywordInfo; +} +const std::vector >& Waf2Transaction::getKvPairs() const +{ + return m_deepParser.kv_pairs; +} +const std::string Waf2Transaction::getSample() const +{ + if (m_scanResult) + { + return m_scanResult->unescaped_line; + } + return std::string(); +} +const std::string Waf2Transaction::getLastScanSample() const +{ + return m_scanner.getLastScanResult().unescaped_line; +} +const std::string& Waf2Transaction::getLastScanParamName() const +{ + return m_scanner.getLastScanResult().param_name; +} +const std::string Waf2Transaction::getKeywordMatchesStr() const +{ + std::vector vec = getKeywordMatches(); + return Waap::Util::vecToString(vec); +} +const std::string Waf2Transaction::getFilteredKeywordsStr() const +{ + std::vector vec = getFilteredKeywords(); + return Waap::Util::vecToString(vec); +} +double Waf2Transaction::getScore() const +{ + if (m_scanResult) { + return m_scanResult->score; + } + return 0; +} +const std::vector Waf2Transaction::getScoreArray() const +{ + if (m_scanResult) { + return m_scanResult->scoreArray; + } + return std::vector(); +} +const std::string Waf2Transaction::getContentTypeStr() const +{ + return m_contentTypeStr; +} +Waap::Util::ContentType Waf2Transaction::getContentType() const +{ + return m_contentType; +} +int Waf2Transaction::getRemotePort() const +{ + return m_remote_port; +} +const std::string Waf2Transaction::getLocalAddress() const +{ + return m_local_addr; +} +int Waf2Transaction::getLocalPort() const +{ + return m_local_port; +} +const std::string Waf2Transaction::getLogTime() const +{ + return m_log_time; +} +ParserBase* Waf2Transaction::getRequestBodyParser() +{ + return m_requestBodyParser; +} +const std::string Waf2Transaction::getMethod() const +{ + return m_methodStr; +} +const std::string Waf2Transaction::getHost() const +{ + return m_hostStr; +} +const std::string Waf2Transaction::getCookie() const +{ + return m_cookieStr; +} +const std::vector Waf2Transaction::getNotes() const +{ + return m_notes; +} +DeepParser& Waf2Transaction::getDeepParser() +{ + return m_deepParser; +} +std::vector > Waf2Transaction::getHdrPairs() const +{ + std::vector > res; + for (auto hdr_pair : hdrs_map) { + res.push_back(std::pair(hdr_pair.first, hdr_pair.second)); + } + return res; +} +const std::string Waf2Transaction::getHdrContent(std::string hdrName) const +{ + boost::algorithm::to_lower(hdrName); + auto hdr_it = hdrs_map.find(hdrName); + if (hdr_it != hdrs_map.end()) { + return hdr_it->second; + } + return ""; +} +const std::string Waf2Transaction::getRequestBody() const +{ + return m_request_body; +} +const std::string Waf2Transaction::getTransactionIdStr() const +{ + return boost::uuids::to_string(m_transaction_id); +} +const std::string Waf2Transaction::getLocation() const +{ + if (m_scanResult) { + return m_scanResult->location; + } + return std::string(); +} +Waap::CSRF::State& Waf2Transaction::getCsrfState() +{ + return m_csrfState; +} + +void Waf2Transaction::sendAutonomousSecurityLog( + const std::shared_ptr& triggerLog, + bool shouldBlock, + const std::string& logOverride, + const std::string& attackTypes) const +{ + auto autonomousSecurityDecision = std::dynamic_pointer_cast( + m_waapDecision.getDecision(AUTONOMOUS_SECURITY_DECISION)); + ReportIS::Severity severity = Waap::Util::computeSeverityFromThreatLevel( + autonomousSecurityDecision->getThreatLevel()); + if (autonomousSecurityDecision->getOverridesLog() && logOverride == OVERRIDE_DROP) + { + severity = ReportIS::Severity::MEDIUM; + } + else if (autonomousSecurityDecision->getOverridesLog() && logOverride == OVERRIDE_ACCEPT) + { + severity = ReportIS::Severity::INFO; + } + + const ReportIS::Priority priority = + Waap::Util::computePriorityFromThreatLevel(autonomousSecurityDecision->getThreatLevel()); + + auto maybeLogTriggerConf = getConfiguration("rulebase", "log"); + LogGenWrapper logGenWrapper( + maybeLogTriggerConf, + "Web Request", + ReportIS::Audience::SECURITY, + LogTriggerConf::SecurityType::ThreatPrevention, + severity, + priority, + shouldBlock); + + LogGen& waap_log = logGenWrapper.getLogGen(); + ThreatLevel threat_level = autonomousSecurityDecision->getThreatLevel(); + if (threat_level != ThreatLevel::NO_THREAT) { + std::string confidence = Waap::Util::computeConfidenceFromThreatLevel(threat_level); + waap_log << LogField("eventConfidence", confidence); + } + + appendCommonLogFields(waap_log, triggerLog, shouldBlock, logOverride, attackTypes); + + std::string sampleString = getSample(); + if (sampleString.length() > MAX_LOG_FIELD_SIZE) { + sampleString.resize(MAX_LOG_FIELD_SIZE); + } + waap_log << LogField("matchedSample", sampleString, LogFieldOption::XORANDB64); + std::string location = getLocation(); + if (location == "url_param") + { + location = "url parameter"; + } + else if (location == "referer_param") + { + location = "referer parameter"; + } + waap_log << LogField("matchedLocation", location); + waap_log << LogField("matchedParameter", getParam()); + + // Patch for reporting of log4j under different name (currently only in logs) + std::vector keywordMatches = getKeywordMatches(); + std::replace(keywordMatches.begin(), keywordMatches.end(), std::string("jndi:"), std::string("java_1")); + std::string keywordMatchesStr = Waap::Util::vecToString(keywordMatches); + + waap_log << LogField("waapFoundIndicators", keywordMatchesStr, LogFieldOption::XORANDB64); + waap_log << LogField("matchedIndicators", keywordMatchesStr, LogFieldOption::XORANDB64); + waap_log << LogField("learnedIndicators", getFilteredKeywordsStr(), LogFieldOption::XORANDB64); + waap_log << LogField("waapUserReputationScore", (int)( + autonomousSecurityDecision->getRelativeReputation() * 100)); + waap_log << LogField("waapUserReputation", getUserReputationStr( + autonomousSecurityDecision->getRelativeReputation())); + waap_log << LogField("waapUriFalsePositiveScore", (int)( + autonomousSecurityDecision->getFpMitigationScore() * 100)); + waap_log << LogField("waapKeywordsScore", (int)(getScore() * 100)); + waap_log << LogField("waapFinalScore", (int)(autonomousSecurityDecision->getFinalScore() * 100)); + waap_log << LogField("waapCalculatedThreatLevel", autonomousSecurityDecision->getThreatLevel()); +} + +void Waf2Transaction::createUserLimitsState() +{ + if (!m_siteConfig || m_userLimitsState || + (WaapConfigBase::get_WebAttackMitigationMode(*m_siteConfig) == AttackMitigationMode::DISABLED)) { + return; + } + + auto userLimitsPolicy = m_siteConfig->get_UserLimitsPolicy(); + if (userLimitsPolicy) { + m_userLimitsState = std::make_shared(*userLimitsPolicy); + m_userLimitsState->setAssetId(m_siteConfig->get_AssetId()); + m_deepParser.setGlobalMaxObjectDepth(userLimitsPolicy->getMaxObjectDepth()); + if (m_uriPath.empty()) { + // Initialize uriPath so it will be available in the sent log, + // in case a limit is reached early in the flow + m_uriPath = m_uriStr.substr(0, LOG_HEADER_MAX_LENGTH); + } + dbgTrace(D_WAAP_ULIMITS) << "[USER LIMITS] state created with '" << + WaapConfigBase::get_WebAttackMitigationModeStr(*m_siteConfig) << "' mode\n" << + *userLimitsPolicy; + } + else { + dbgTrace(D_WAAP_ULIMITS) << "[USER LIMITS] couldn't load policy"; + } +} + +ngx_http_cp_verdict_e +Waf2Transaction::getUserLimitVerdict() +{ + if (!isUserLimitReached()) { + // Either limit not reached or attack mitigation mode is DISABLED + return ngx_http_cp_verdict_e::TRAFFIC_VERDICT_INSPECT; + } + + std::string msg; + msg = "[USER LIMITS][" + + std::string(WaapConfigBase::get_WebAttackMitigationModeStr(*m_siteConfig)) + + " mode] " + "Verdict is "; + std::string reason; + reason = " reason: " + getViolatedUserLimitTypeStr(); + + ngx_http_cp_verdict_e verdict = ngx_http_cp_verdict_e::TRAFFIC_VERDICT_INSPECT; + const AttackMitigationMode mode = WaapConfigBase::get_WebAttackMitigationMode(*m_siteConfig); + auto decision = m_waapDecision.getDecision(USER_LIMITS_DECISION); + if (mode == AttackMitigationMode::LEARNING) { + decision->setLog(true); + decision->setBlock(false); + if (isIllegalMethodViolation()) { + dbgInfo(D_WAAP_ULIMITS) << msg << "INSPECT" << reason << " in detect mode"; + verdict = ngx_http_cp_verdict_e::TRAFFIC_VERDICT_INSPECT; + } + else { + dbgInfo(D_WAAP_ULIMITS) << msg << "PASS" << reason; + verdict = ngx_http_cp_verdict_e::TRAFFIC_VERDICT_ACCEPT; + } + } + else if (mode == AttackMitigationMode::PREVENT) { + decision->setLog(true); + decision->setBlock(true); + dbgInfo(D_WAAP_ULIMITS) << msg << "BLOCK" << reason; + verdict = ngx_http_cp_verdict_e::TRAFFIC_VERDICT_DROP; + } + + return verdict; +} + +const std::string Waf2Transaction::getUserLimitVerdictStr() const +{ + std::stringstream verdict; + if (!isUserLimitReached()) { + verdict << getViolatedUserLimitTypeStr(); + } + else if (isIllegalMethodViolation()) { + verdict << getViolatedUserLimitTypeStr() << " (" << getMethod() << ")"; + } + else { + auto strData = getViolatedUserLimitStrData(); + verdict << strData.type << " (" << getViolatingUserLimitSize() << + "/" << strData.policy << ")"; + } + return verdict.str(); +} + +bool Waf2Transaction::isUrlLimitReached(size_t size) +{ + if (!m_userLimitsState) { + return false; + } + return m_userLimitsState->addUrlBytes(size); +} +bool Waf2Transaction::isHttpHeaderLimitReached(const std::string& name, const std::string& value) +{ + if (!m_userLimitsState) { + return false; + } + return m_userLimitsState->addHeaderBytes(name, value); +} +bool Waf2Transaction::isHttpBodyLimitReached(size_t chunkSize) +{ + if (!m_userLimitsState) { + return false; + } + return m_userLimitsState->addBodyBytes(chunkSize); +} +bool Waf2Transaction::isObjectDepthLimitReached(size_t depth) +{ + if (!m_userLimitsState) { + return false; + } + return m_userLimitsState->setObjectDepth(depth); +} +bool Waf2Transaction::isPreventModeValidMethod(const std::string& method) +{ + if (!m_userLimitsState) { + return true; + } + + if (m_userLimitsState->isValidHttpMethod(method) || + (WaapConfigBase::get_WebAttackMitigationMode(*m_siteConfig) == AttackMitigationMode::LEARNING)) { + return true; + } + return false; +} +bool Waf2Transaction::isUserLimitReached() const +{ + return m_userLimitsState ? m_userLimitsState->isLimitReached() : false; +} +bool Waf2Transaction::isIllegalMethodViolation() const +{ + return m_userLimitsState ? m_userLimitsState->isIllegalMethodViolation() : false; +} +const std::string Waf2Transaction::getViolatedUserLimitTypeStr() const +{ + return m_userLimitsState ? m_userLimitsState->getViolatedTypeStr() : "no enforcement"; +} +const Waap::UserLimits::ViolatedStrData& +Waf2Transaction::getViolatedUserLimitStrData() const +{ + return m_userLimitsState->getViolatedStrData(); +} +size_t Waf2Transaction::getViolatingUserLimitSize() const +{ + return m_userLimitsState ? m_userLimitsState->getViolatingSize() : 0; +} + +const std::set Waf2Transaction::getFoundPatterns() const +{ + return m_found_patterns; +} + +Waap::Override::State Waf2Transaction::getOverrideState(IWaapConfig* sitePolicy) +{ + Waap::Override::State overrideState; + std::shared_ptr overridePolicy = sitePolicy->get_OverridePolicy(); + if (overridePolicy) { // at first we will run request overrides (in order to set the source) + overrideState.applyOverride(*overridePolicy, WaapOverrideFunctor(*this), m_matchedOverrideIds, true); + } + + extractEnvSourceIdentifier(); + + Waap::Override::State overrideStateResponse; + if (overridePolicy) { // later we will run response overrides + overrideStateResponse.applyOverride(*overridePolicy, WaapOverrideFunctor(*this), m_matchedOverrideIds, false); + } + return overrideStateResponse; +} + +Waf2TransactionFlags &Waf2Transaction::getTransactionFlags() +{ + return m_waf2TransactionFlags; +} + +const std::shared_ptr Waf2Transaction::getTriggerLog(const std::shared_ptr< + Waap::Trigger::Policy> &triggerPolicy) const +{ + // Trigger log already known (no need to extract it second time) + if (m_triggerLog) { + return m_triggerLog; + } + + // Walk over trigger logs and choose the last one of type Log + for (const Waap::Trigger::Trigger &trigger : triggerPolicy->triggers) { + if (trigger.triggerType == "log") { + m_triggerLog = trigger.log; + } + } + + return m_triggerLog; +} + +ReportIS::Severity Waf2Transaction::computeEventSeverityFromDecision() const +{ + DecisionType type = m_waapDecision.getHighestPriorityDecisionToLog(); + switch (type) + { + case DecisionType::USER_LIMITS_DECISION: + { + return ReportIS::Severity::HIGH; + break; + } + case DecisionType::OPEN_REDIRECT_DECISION: + case DecisionType::ERROR_LIMITING_DECISION: + case DecisionType::RATE_LIMITING_DECISION: + case DecisionType::CSRF_DECISION: + case DecisionType::ERROR_DISCLOSURE_DECISION: + { + return ReportIS::Severity::CRITICAL; + break; + } + case DecisionType::AUTONOMOUS_SECURITY_DECISION: + { + auto autonomousSecurityDecision = std::dynamic_pointer_cast( + m_waapDecision.getDecision(DecisionType::AUTONOMOUS_SECURITY_DECISION)); + return Waap::Util::computeSeverityFromThreatLevel(autonomousSecurityDecision->getThreatLevel()); + } + default: + static_assert(true, "Illegal DecisionType enum value"); + break; + } + + return ReportIS::Severity::INFO; +} diff --git a/components/security_apps/waap/waap_clib/Waf2Regex.cc b/components/security_apps/waap/waap_clib/Waf2Regex.cc new file mode 100755 index 0000000..b6dfe02 --- /dev/null +++ b/components/security_apps/waap/waap_clib/Waf2Regex.cc @@ -0,0 +1,653 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// #define WAF2_LOGGING_ENABLE + +#include "Waf2Regex.h" +#include "debug.h" +#include +#include + +USE_DEBUG_FLAG(D_WAAP_REGEX); + +// SingleRegex + +SingleRegex::SingleRegex( + const std::string& pattern, + bool& error, + const std::string& regexName, + bool bNoRegex, + const std::string ®exMatchName, + const std::string ®exMatchValue) + : + m_re(NULL), + m_matchData(NULL), + m_regexName(regexName), + m_noRegex(bNoRegex), + m_regexMatchName(regexMatchName), + m_regexMatchValue(regexMatchValue) + { + dbgTrace(D_WAAP_REGEX) << "Create SingleRegex '" << m_regexName << "' PATTERN: '" << + std::string(pattern.data(), pattern.size()) << "'"; + + if (error) { + // Skip initialization if already in error condition + dbgError(D_WAAP_REGEX) << "Skip compiling regex: " << m_regexName << " (single) due to previous error"; + return; + } + + int errorCode; + size_t errorOffset; + m_re = pcre2_compile( + reinterpret_cast(pattern.data()), + pattern.size(), + 0, + &errorCode, + &errorOffset, + NULL + ); + + if (pcre2_jit_compile(m_re, PCRE2_JIT_COMPLETE) < 0) { + dbgError(D_WAAP_REGEX) << "pcre2_jit_compile failed for regex: " << m_regexName << " (single)"; + error = true; + } + + if (m_re == NULL) { + PCRE2_UCHAR errMessage[4096]; + pcre2_get_error_message(errorCode, errMessage, sizeof(errMessage)); + dbgError(D_WAAP_REGEX) << "pcre2_compile failed: error (" << errorCode << "), " << errMessage << + ", at offset " << errorOffset << " in pattern (single) of regex " << m_regexName << "."; + dbgError(D_WAAP_REGEX) << "pattern: '" << pattern.c_str() << "'"; + error = true; + return; + } + + // Create matchData object that is ready to receive any possible match from m_re + m_matchData = pcre2_match_data_create_from_pattern(m_re, NULL); + + if (m_matchData == NULL) { + dbgError(D_WAAP_REGEX) << "pcre2_compile failed to allocate matchData. pattern: '" << + std::string(pattern.data(), pattern.size()) << "'"; + pcre2_code_free(m_re); + m_re = NULL; + return; + } + + // Get info about compiled pattern + pcre2_pattern_info(m_re, PCRE2_INFO_CAPTURECOUNT, &m_captureGroupsCount); + PCRE2_SPTR nameTable; + uint32_t nameCount; + uint32_t nameEntrySize; + pcre2_pattern_info(m_re, PCRE2_INFO_NAMECOUNT, &nameCount); + pcre2_pattern_info(m_re, PCRE2_INFO_NAMEENTRYSIZE, &nameEntrySize); + pcre2_pattern_info(m_re, PCRE2_INFO_NAMETABLE, &nameTable); + + // Allocate enough items for group names to be indexed by capture group index + // Note that number capture groups are numbered starting from 1. Group "0" is for the "whole match" + m_captureNames.resize(m_captureGroupsCount + 1); + + for (uint32_t i = 0; i < nameCount; i++) { + PCRE2_SPTR nameTableEntry = nameTable + i * nameEntrySize; + // According to pcre2 docs, each entry struct starts with 16-bit capture index (big-endian). Consume it. + uint16_t captureIndex = (nameTableEntry[0] << 8) + nameTableEntry[1]; + // Note that capture group indices are numbered starting from 1. Group "0" is for the "whole match" + nameTableEntry += sizeof(uint16_t); + // After the index comes zero-terminated capture name. Consume it too. + m_captureNames[captureIndex] = (char*)nameTableEntry; + } +} + +SingleRegex::~SingleRegex() { + if (m_matchData) { + pcre2_match_data_free(m_matchData); + } + + if (m_re) { + pcre2_code_free(m_re); + } +} + +bool SingleRegex::hasMatch(const std::string& s) const { + int rc = pcre2_match( + m_re, // code + reinterpret_cast(s.data()), s.size(), // subject/subject length + 0, // start offset + 0, // options + m_matchData, + NULL // match_context + ); + + if (rc <= 0) { + if (rc != PCRE2_ERROR_NOMATCH) { + PCRE2_UCHAR errmsg[4096]; + pcre2_get_error_message(rc, errmsg, sizeof(errmsg) - 1); + dbgDebug(D_WAAP_REGEX) << "SingleRegex['" << m_regexName << "']::hasMatch " << + "failed with error code: " << rc << " ('" << errmsg << "')"; + } + return false; + } + + return true; +} + +size_t SingleRegex::findAllMatches(const std::string& s, std::vector& matches) const { + size_t matchesCount = 0; + + // Optimized regex that always immediately reports a "simulated" match without spending time to do a scan + if (m_noRegex) { + RegexMatch match; + // Group 0 is "whole match" must always be present and have no name + match.groups.push_back( + RegexMatch::MatchGroup( + 1, + "", + m_regexMatchValue + ) + ); + // Group 1 is "specific match" must be present and have a name + match.groups.push_back( + RegexMatch::MatchGroup( + 2, + m_regexMatchName, + m_regexMatchValue + ) + ); + matches.push_back(match); + matchesCount++; + return matchesCount; + } + + PCRE2_SIZE startOffset = 0; + + do { + int rc = pcre2_match( + m_re, // code + reinterpret_cast(s.data()), s.size(), // subject/subject length + startOffset, // start offset + 0, // options + m_matchData, + NULL // match_context + ); + + if (rc <= 0) { + if (rc != PCRE2_ERROR_NOMATCH) { + PCRE2_UCHAR errmsg[4096]; + pcre2_get_error_message(rc, errmsg, sizeof(errmsg) - 1); + dbgDebug(D_WAAP_REGEX) << "SingleRegex['" << m_regexName << "']::findAllMatches " << + "failed with error code: " << rc << " ('" << errmsg << "')"; + } + break; + } + + int highestMatchedGroupIndex = rc; + + // Get pointer to array of offsets into s, and its size + uint32_t ovCount = pcre2_get_ovector_count(m_matchData); + PCRE2_SIZE* ov = pcre2_get_ovector_pointer(m_matchData); + + RegexMatch match; + match.groups.reserve(ovCount); + + dbgTrace(D_WAAP_REGEX) << "regex '" << m_regexName << "', captureGroupsCount = " << + m_captureGroupsCount << ". ovCount = " << ovCount << "; highestMatchedGroupIndex = " << + highestMatchedGroupIndex; + + // ov is vector of ovCount pairs of PCRE2_SIZE values. + // First entry in pair is offset of start of the match (in s), + // second entry is offset of character one after end of the match. + // Walk over all matches and fill them here (-1 because first one isn't included in ovCount). + for (int groupIndex = 1; groupIndex < highestMatchedGroupIndex; ++groupIndex) { + PCRE2_SIZE rangeStart = ov[groupIndex * 2]; + PCRE2_SIZE rangeEnd = ov[groupIndex * 2 + 1]; + + // Skip matches that are not set + if (rangeStart == PCRE2_UNSET || rangeEnd == PCRE2_UNSET) { + continue; + } + + dbgTrace(D_WAAP_REGEX) << "groupIndex=" << groupIndex << " ['" << m_captureNames[groupIndex] << + "']: range " << rangeStart << " -> " << rangeEnd; + match.groups.push_back( + RegexMatch::MatchGroup( + groupIndex, + m_captureNames[groupIndex], + s.substr(rangeStart, rangeEnd - rangeStart) + ) + ); + } + + matches.push_back(match); + + // Count matches found in this SingleRegex + matchesCount++; + + // continue searching for next match starting from end of this match + // (first two entries in ov[] are start and end offsets of current full match) + startOffset = ov[1]; + } while (true); + + return matchesCount; +} + +const std::string &SingleRegex::getName() const +{ + return m_regexName; +} + +size_t SingleRegex::findMatchRanges(const std::string& s, std::vector& matchRanges) const { + PCRE2_SIZE startOffset = 0; + + do { + int rc = pcre2_match( + m_re, // code + reinterpret_cast(s.data()), s.size(), // subject/subject length + startOffset, // start offset + 0, // options + m_matchData, + NULL // match_context + ); + + // Note: PCRE2_ERROR_NOMATCH is the normal situation here, but there could be other errors. + // However, whichever error occurred, the loop is stopped. + if (rc <= 0) { + if (rc != PCRE2_ERROR_NOMATCH) { + PCRE2_UCHAR errmsg[4096]; + pcre2_get_error_message(rc, errmsg, sizeof(errmsg) - 1); + dbgDebug(D_WAAP_REGEX) << "SingleRegex['" << m_regexName << "']::findMatchRanges " << + "failed with error code: " << rc << " ('" << errmsg << "')"; + } + break; + } + + // Get pointer to array of offsets into s + PCRE2_SIZE* ov = pcre2_get_ovector_pointer(m_matchData); + + // start searching for next match starting from end of this match + // (first two entries in ov[] are start and end offsets of current full match) + startOffset = ov[1]; + + matchRanges.push_back(RegexMatchRange(ov[0], ov[1])); + } while (true); + + return matchRanges.size(); +} + +// Regex + +Regex::Regex(const std::string& pattern, bool &error, const std::string& regexName) +: +m_regexName(regexName), +m_regexPreconditions(nullptr) // no need for preconditions for single regex mode +{ + if (error) { + // Skip initialization if already in error condition + dbgError(D_WAAP_REGEX) << "Skip compiling regex: " << m_regexName << " (single) due to previous error"; + return; + } + + m_sre.push_back(new SingleRegex(pattern, error, m_regexName)); +} + +// Divide regexp patterns longer than the limit (imposed by pcre2 library!) into multiple regexes. +#define REGEX_PATT_MAX_SIZE 0 + +Regex::Regex( + const std::vector & patterns, + bool &error, + const std::string & regexName, + std::shared_ptr regexPreconditions) +: +m_regexName(regexName), +m_regexPreconditions(regexPreconditions) +{ + if (error) { + // Skip initialization if already in error condition + dbgError(D_WAAP_REGEX) << "Skip compiling regex: " << m_regexName << " due to previous error"; + return; + } + + // This regex helps to parse out group names from regex patterns + SingleRegex patternParseRegex("^\\(\\?P<(.*?)>(.*?)\\)$", error, "patternParseRegex"); + + std::string acc; + + for (std::vector::const_iterator pPattern = patterns.begin(); + pPattern != patterns.end(); + ++pPattern) { + const std::string& pattern = *pPattern; + if ((acc.size() + pattern.size()) > REGEX_PATT_MAX_SIZE) { + if (!acc.empty()) { + assert(false); // this should never happen + m_sre.push_back(new SingleRegex(acc + ")", error, m_regexName)); + acc = "(" + pattern; + } + else + { + bool bNoRegex = false; + std::string regexMatchName; + std::string regexMatchValue; + + // This is the only place where patterns are loaded (one-by-one) + if (m_regexPreconditions) { + // If preconditions are enabled on this Regex instance - build list of indices of SingleRegex + // that should be triggered (executed) for each related word found by aho-corasick pattern scan. + Waap::RegexPreconditions::WordIndex wordIndex = + m_regexPreconditions->getWordByRegex(pattern); + + // Extract group name from the regex pattern string + if (m_regexPreconditions->isNoRegexPattern(pattern)) { + // This word should not be scanned with regex. Instead, it should directly return a match + std::vector parsedMatches; + patternParseRegex.findAllMatches(pattern, parsedMatches); + bNoRegex = true; + regexMatchName = parsedMatches[0].groups[0].value; + regexMatchValue = m_regexPreconditions->getWordStrByWordIndex(wordIndex); + } + + // For each word - build list of SingleRegex indices to be scanned if that word is detected + // Note that if aho-corasick word for this regex is not yet defined it will enter the [""] entry + // and will always be executed. This is less efficient but ensures correct attack detection. + m_wordToRegexIndices[wordIndex].push_back(m_sre.size()); + } + else { + // If preconditions are not enabled on this Regex instance - all SingleRegexes in it will always + // be executed. + m_wordToRegexIndices[Waap::RegexPreconditions::emptyWordIndex].push_back(m_sre.size()); + } + + m_sre.push_back(new SingleRegex("(" + pattern+ ")", error, m_regexName + "/" + pattern, bNoRegex, + regexMatchName, regexMatchValue)); + } + } + else { + assert(false); // this should never happen anymore. + // Add | character between individual patterns, but not before the very first one! + if (acc.empty()) { + // first group + acc = "(" + pattern; + } + else { + // non-first group + acc += "|" + pattern; + } + } + } + + if (acc.size() > 0) { + assert(false); // this should never happen anymore. + m_sre.push_back(new SingleRegex(acc + ")", error, m_regexName)); + } +} + +Regex::~Regex() { + for (std::vector::iterator ppSingleRegex = m_sre.begin(); + ppSingleRegex != m_sre.end(); + ++ppSingleRegex) { + SingleRegex* pSingleRegex = *ppSingleRegex; + + if (pSingleRegex) { + delete pSingleRegex; + } + } +} + +bool Regex::hasMatch(const std::string& s) const { + for (std::vector::const_iterator ppSingleRegex = m_sre.begin(); + ppSingleRegex != m_sre.end(); + ++ppSingleRegex) { + SingleRegex* pSingleRegex = *ppSingleRegex; + + if (pSingleRegex->hasMatch(s)) { + dbgTrace(D_WAAP_REGEX) << "Regex['" << m_regexName << "']['" << pSingleRegex->getName() << + "']::hasMatch() found!"; + return true; + } + } + + return false; +} + +size_t Regex::findAllMatches(const std::string& s, std::vector& matches, + const Waap::RegexPreconditions::PmWordSet *pmWordSet) const { + matches.clear(); + + if (m_regexPreconditions && pmWordSet) { + // If preconditions are enabled on this regex - execute them to make scanning more efficient + std::unordered_set dupIndices; + + for (Waap::RegexPreconditions::WordIndex wordIndex : *pmWordSet) { + const auto &found = m_wordToRegexIndices.find(wordIndex); + + // Check that the wordIndex is related to this instance of Regex object + if (found == m_wordToRegexIndices.end()) { + continue; + } + + const std::vector ®exIndicesList = found->second; + + for (size_t regexIndex : regexIndicesList) { + if (dupIndices.find(regexIndex) != dupIndices.end()) { + // Avoid scanning the same regex index twice (in case it is registered for more than one wordIndex) + continue; + } + + // Scan only regexes that are enabled by aho-corasick scan + m_sre[regexIndex]->findAllMatches(s, matches); + dbgTrace(D_WAAP_REGEX) << "Regex['" << m_sre[regexIndex]->getName() << + "',index=" << regexIndex << "]::findAllMatches(): " << matches.size() << " matches found (so far)"; + + dupIndices.insert(regexIndex); + } + } + } + else { + // When optimization is disabled - scan all regexes + for (SingleRegex* pSingleRegex : m_sre) { + pSingleRegex->findAllMatches(s, matches); + dbgTrace(D_WAAP_REGEX) << "Regex['" << m_regexName << "']['" << pSingleRegex->getName() << + "']::findAllMatches(): " << matches.size() << " matches found (so far)"; + } + } + + dbgTrace(D_WAAP_REGEX) << "Regex['" << m_regexName << "']::findAllMatches(): total " << + matches.size() << " matches found."; + return matches.size(); +} + +inline bool consolidateMatchRangesSortFunc(const RegexMatchRange& a, const RegexMatchRange& b) { + return a.start > b.start; +} + +// Consolidate ranges in-place (algorithm adapted from this solution: +// http://www.geeksforgeeks.org/merging-intervals) +static void consolidateMatchRanges(std::vector& matchRanges) { + // Sort ranges in decreasing order of their start offsets (O(logN) time) + std::sort(matchRanges.begin(), matchRanges.end(), consolidateMatchRangesSortFunc); + int lastIndex = 0; // index of last range in matchRanges vector (up to this range everything is merged) + + // Traverse all ranges and merge where necessary + for (size_t i = 0; i < matchRanges.size(); ++i) { + // If this is not first range and it overlaps with the previous range + if (lastIndex != 0 && matchRanges[lastIndex - 1].start < matchRanges[i].end) { + while (lastIndex != 0 && matchRanges[lastIndex - 1].start < matchRanges[i].end) { + // merge previous and current ranges + matchRanges[lastIndex - 1].end = std::max(matchRanges[lastIndex - 1].end, matchRanges[i].end); + matchRanges[lastIndex - 1].start = std::min(matchRanges[lastIndex - 1].start, matchRanges[i].start); + lastIndex--; + } + } + else { + // Doesn't overlap with previous (or no previous because this is first range), + // add the range as-is + matchRanges[lastIndex] = matchRanges[i]; + } + + lastIndex++; + } + + // Keep only merged ranges. Erase extra ranges that are not used anymore + matchRanges.resize(lastIndex); +} + +std::string Regex::sub(const std::string& s, const std::string& repl) const { + std::vector matchRanges; + + // Find all ranges of all matches + for (std::vector::const_iterator ppSingleRegex = m_sre.begin(); + ppSingleRegex != m_sre.end(); + ++ppSingleRegex) { + SingleRegex* pSingleRegex = *ppSingleRegex; + pSingleRegex->findMatchRanges(s, matchRanges); +#ifdef WAF2_LOGGING_ENABLE + dbgTrace(D_WAAP_REGEX) << "Regex['" << m_regexName << "']['" << pSingleRegex->getName() << + "']::sub(): " << matchRanges.size() << " match ranges found (so far):"; + for (size_t i = 0; i < matchRanges.size(); ++i) { + dbgTrace(D_WAAP_REGEX) << "Range [" << i << "]: " << matchRanges[i].start << " -> " << matchRanges[i].end; + } +#endif + } + + // No matches - nothing to replace. + if (matchRanges.empty()) { + return s; + } + + // Match ranges collected from multiple single regexps could overlap and be out of order + // This function sorts the ranges in place (in decreasing order) and also consolidates overlapping + // ranges so they do not overlap. + consolidateMatchRanges(matchRanges); + +#ifdef WAF2_LOGGING_ENABLE + dbgTrace(D_WAAP_REGEX) << "Regex['" << m_regexName << "']::sub(): " << + matchRanges.size() << " match ranges (after consolidation):"; + for (size_t i = 0; i < matchRanges.size(); ++i) { + dbgTrace(D_WAAP_REGEX) << "Range [" << i << "]: " << matchRanges[i].start << " -> " << matchRanges[i].end; + } +#endif + + // Now walk over (consolidated) ranges (that are now guaranteed not to overlap), and copy everything around them + // Note that ranges are still sorted in decreasing order, so we traverse the list backwards to see them in + // increasing order + PCRE2_SIZE startOffset = 0; + std::string outStr; + + for (std::vector::const_reverse_iterator pMatchRange = matchRanges.rbegin(); + pMatchRange != matchRanges.rend(); + ++pMatchRange) { + // Add everything since startOffset until start of current range + outStr += s.substr(startOffset, pMatchRange->start - startOffset); + + // Add replacement + if (!repl.empty()) { + outStr += repl; + } + // Keep copying only after end of current range + startOffset = pMatchRange->end; + } + + // Add remainder of string after last range + outStr += s.substr(startOffset); + return outStr; +} + +// TODO:: refactor out with C++ functor instead of C-style pointer-callback! +void +Regex::sub( + const std::string& s, + Waap::Util::RegexSubCallback_f cb, + int& decodedCount, + int& deletedCount, + std::string& outStr) const +{ + decodedCount = 0; + deletedCount = 0; + + // Clear outStr, it will be filled with output string (with changes, if applicable) + outStr.clear(); + + std::vector matchRanges; + + // Find all ranges of all matches + for (std::vector::const_iterator ppSingleRegex = m_sre.begin(); + ppSingleRegex != m_sre.end(); + ++ppSingleRegex) { + SingleRegex* pSingleRegex = *ppSingleRegex; + pSingleRegex->findMatchRanges(s, matchRanges); +#ifdef WAF2_LOGGING_ENABLE + dbgTrace(D_WAAP_REGEX) << "Regex['" << m_regexName << "']['" << pSingleRegex->getName() + << "']::sub(): " << matchRanges.size() << " match ranges found (so far):"; + for (size_t i = 0; i < matchRanges.size(); ++i) { + dbgTrace(D_WAAP_REGEX) << "Range [" << i << "]: " << matchRanges[i].start << " -> " << matchRanges[i].end; + } +#endif + } + + // No matches - nothing to replace. + if (matchRanges.empty()) { + outStr = s; + return; + } + + // Match ranges collected from multiple single regexps could overlap and be out of order + // This function sorts the ranges in place (in decreasing order) and also consolidates + // overlapping ranges so they do not overlap. + consolidateMatchRanges(matchRanges); + +#ifdef WAF2_LOGGING_ENABLE + dbgTrace(D_WAAP_REGEX) << "Regex['" << m_regexName << "']::sub(): " << + matchRanges.size() << " match ranges (after consolidation):"; + for (size_t i = 0; i < matchRanges.size(); ++i) { + dbgTrace(D_WAAP_REGEX) << "Range [" << i << "]: " << matchRanges[i].start << " -> " << matchRanges[i].end; + } +#endif + + // Now walk over (consolidated) ranges (that are now guaranteed not to overlap), and copy everything around them + // Note that ranges are still sorted in decreasing order, so we traverse the list backwards to see them in + // increasing order + PCRE2_SIZE startOffset = 0; + + for (std::vector::const_reverse_iterator pMatchRange = matchRanges.rbegin(); + pMatchRange != matchRanges.rend(); + ++pMatchRange) { + // Add everything since startOffset until start of current range + outStr += s.substr(startOffset, pMatchRange->start - startOffset); + + // Compute replacement + std::string repl; + if (cb(s, s.begin() + pMatchRange->start, s.begin() + pMatchRange->end, repl)) { + if (!repl.empty()) { + outStr += repl; + decodedCount++; + } + else { + deletedCount++; + } + } + else { + // if callback told us the chunk was not processed - put original text inside + outStr += s.substr(pMatchRange->start, pMatchRange->end - pMatchRange->start); + } + + // Keep copying only after end of current range + startOffset = pMatchRange->end; + } + + // Add remainder of string after last range + outStr += s.substr(startOffset); + return; +} + +const std::string &Regex::getName() const +{ + return m_regexName; +} diff --git a/components/security_apps/waap/waap_clib/Waf2Regex.h b/components/security_apps/waap/waap_clib/Waf2Regex.h new file mode 100755 index 0000000..8b80292 --- /dev/null +++ b/components/security_apps/waap/waap_clib/Waf2Regex.h @@ -0,0 +1,100 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __WAF2_REGEX_H__c31bc34a +#define __WAF2_REGEX_H__c31bc34a + +// Note: good usage reference found here: http://codegists.com/snippet/c/pcre2_matchcpp_neurobin_c +// and also here https://svn.apache.org/repos/asf/httpd/httpd/trunk/server/util_pcre.c + +#define PCRE2_CODE_UNIT_WIDTH 8 +#include "Waf2Util.h" +#include "WaapRegexPreconditions.h" +#include +#include +#include +#include + +struct RegexMatch { + struct MatchGroup { + uint16_t index; + std::string name; + std::string value; + + MatchGroup(uint16_t index, const std::string &name, const std::string &value) + :index(index), name(name), value(value) { + } + }; + + std::vector groups; +}; + +struct RegexMatchRange { + PCRE2_SIZE start; + PCRE2_SIZE end; +// LCOV_EXCL_START Reason: coverage upgrade + RegexMatchRange() {} +// LCOV_EXCL_STOP + RegexMatchRange(PCRE2_SIZE start, PCRE2_SIZE end):start(start), end(end) {} +}; + +class SingleRegex : public boost::noncopyable { +friend class Regex; +public: + SingleRegex(const std::string &pattern, bool &error, const std::string ®exName, bool bNoRegex=false, + const std::string ®exMatchName="", const std::string ®exMatchValue=""); + ~SingleRegex(); + bool hasMatch(const std::string &s) const; + size_t findAllMatches(const std::string &s, std::vector &matches) const; + size_t findMatchRanges(const std::string &s, std::vector &matchRanges) const; + const std::string &getName() const; +private: + pcre2_code *m_re; + pcre2_match_data *m_matchData; + uint32_t m_captureGroupsCount; + std::vector m_captureNames; // capture index => name translation (unnamed items are empty strings) + std::string m_regexName; + bool m_noRegex; + std::string m_regexMatchName; + std::string m_regexMatchValue; +}; + +class Regex : public boost::noncopyable { +public: + Regex(const std::string &pattern, bool &error, const std::string ®exName); + Regex(const std::vector &patterns, bool &error, const std::string ®exName, + std::shared_ptr regexPreconditions); + ~Regex(); + bool hasMatch(const std::string &s) const; + size_t findAllMatches(const std::string &v, std::vector &maches, + const Waap::RegexPreconditions::PmWordSet *pmWordSet=nullptr) const; + std::string sub(const std::string &s, const std::string &repl="") const; + // Run regex search, and for each found match - run callback. + // The callback can cancel replacement of the match (leave source match "as-is"), provide a replacement string, + // or delete the match (replace with empty string). + // The "decodedCount" counts match replacement events and the "deletedCount" counts match deletion events. + void sub( + const std::string &s, + Waap::Util::RegexSubCallback_f cb, + int &decodedCount, + int &deletedCount, + std::string &outStr) const; + const std::string &getName() const; +private: + std::vector m_sre; + std::string m_regexName; + std::shared_ptr m_regexPreconditions; + std::unordered_map> m_wordToRegexIndices; +}; + +#endif // __WAF2_REGEX_H__c31bc34a diff --git a/components/security_apps/waap/waap_clib/Waf2Util.cc b/components/security_apps/waap/waap_clib/Waf2Util.cc new file mode 100755 index 0000000..79cba75 --- /dev/null +++ b/components/security_apps/waap/waap_clib/Waf2Util.cc @@ -0,0 +1,1921 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "Waf2Util.h" + +#include "debug.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "CidrMatch.h" +#include "debug.h" +#include "config.h" +#include "generic_rulebase/rulebase_config.h" +#include "user_identifiers_config.h" +#include "Waf2Regex.h" + +using boost::algorithm::to_lower_copy; +using namespace std; + +USE_DEBUG_FLAG(D_WAAP); +USE_DEBUG_FLAG(D_WAAP_EVASIONS); + +#define MIN_HEX_LENGTH 6 +#define charToDigit(c) (c - '0') + +// See https://dev.w3.org/html5/html-author/charref +const struct HtmlEntity g_htmlEntities[] = +{ + {"Tab;", 0x0009}, + {"NewLine;", 0x000A}, + {"nbsp;", 0x00A0}, + {"NonBreakingSpace;", 0x00A0}, + {"excl;", 0x0021}, + {"num;", 0x0023}, + {"dollar;", 0x0024}, + {"percnt;", 0x0025}, + {"lpar;", 0x0028}, + {"rpar;", 0x0029}, + {"ast;", 0x002A}, + {"midast;", 0x002A}, + {"plus;", 0x002B}, + {"comma;", 0x002C}, + {"period;", 0x002E}, + {"sol;", 0x002F}, + {"colon;", 0x003A}, + {"semi;", 0x003B}, + {"iexcl;", 0x00A1}, + {"cent;", 0x00A2}, + {"pound;", 0x00A3}, + {"curren;", 0x00A4}, + {"yen;", 0x00A5}, + {"brvbar;", 0x00A6}, + {"sect;", 0x00A7}, + {"uml;", 0x00A8}, + {"copy;", 0x00A9}, + {"ordf;", 0x00AA}, + {"laquo;", 0x00AB}, + {"not;", 0x00AC}, + {"shy;", 0x00AD}, + {"reg;", 0x00AE}, + {"macr;", 0x00AF}, + {"deg;", 0x00B0}, + {"plusmn;", 0x00B1}, + {"sup2;", 0x00B2}, + {"sup3;", 0x00B3}, + {"acute;", 0x00B4}, + {"micro;", 0x00B5}, + {"para;", 0x00B6}, + {"middot;", 0x00B7}, + {"cedil;", 0x00B8}, + {"sup1;", 0x00B9}, + {"ordm;", 0x00BA}, + {"raquo;", 0x00BB}, + {"frac14;", 0x00BC}, + {"frac12;", 0x00BD}, + {"frac34;", 0x00BE}, + {"iquest;", 0x00BF}, + {"Agrave;", 0x00C0}, + {"Aacute;", 0x00C1}, + {"Acirc;", 0x00C2}, + {"Atilde;", 0x00C3}, + {"Auml;", 0x00C4}, + {"Aring;", 0x00C5}, + {"AElig;", 0x00C6}, + {"Ccedil;", 0x00C7}, + {"Egrave;", 0x00C8}, + {"Eacute;", 0x00C9}, + {"Ecirc;", 0x00CA}, + {"Euml;", 0x00CB}, + {"Igrave;", 0x00CC}, + {"Iacute;", 0x00CD}, + {"Icirc;", 0x00CE}, + {"Iuml;", 0x00CF}, + {"ETH;", 0x00D0}, + {"Ntilde;", 0x00D1}, + {"Ograve;", 0x00D2}, + {"Oacute;", 0x00D3}, + {"Ocirc;", 0x00D4}, + {"Otilde;", 0x00D5}, + {"Ouml;", 0x00D6}, + {"times;", 0x00D7}, + {"Oslash;", 0x00D8}, + {"Ugrave;", 0x00D9}, + {"Uacute;", 0x00DA}, + {"Ucirc;", 0x00DB}, + {"Uuml;", 0x00DC}, + {"Yacute;", 0x00DD}, + {"THORN;", 0x00DE}, + {"szlig;", 0x00DF}, + {"agrave;", 0x00E0}, + {"aacute;", 0x00E1}, + {"acirc;", 0x00E2}, + {"atilde;", 0x00E3}, + {"auml;", 0x00E4}, + {"aring;", 0x00E5}, + {"aelig;", 0x00E6}, + {"ccedil;", 0x00E7}, + {"egrave;", 0x00E8}, + {"eacute;", 0x00E9}, + {"ecirc;", 0x00EA}, + {"euml;", 0x00EB}, + {"igrave;", 0x00EC}, + {"iacute;", 0x00ED}, + {"icirc;", 0x00EE}, + {"iuml;", 0x00EF}, + {"eth;", 0x00F0}, + {"ntilde;", 0x00F1}, + {"ograve;", 0x00F2}, + {"oacute;", 0x00F3}, + {"ocirc;", 0x00F4}, + {"otilde;", 0x00F5}, + {"ouml;", 0x00F6}, + {"divide;", 0x00F7}, + {"oslash;", 0x00F8}, + {"ugrave;", 0x00F9}, + {"uacute;", 0x00FA}, + {"ucirc;", 0x00FB}, + {"uuml;", 0x00FC}, + {"yacute;", 0x00FD}, + {"thorn;", 0x00FE}, + {"yuml;", 0x00FF}, + {"quot;", 0x0022}, + {"amp;", 0x0026}, + {"lt;", 0x003C}, + {"LT;", 0x003C}, + {"equals;", 0x003D}, + {"gt;", 0x003E}, + {"GT;", 0x003E}, + {"quest;", 0x003F}, + {"commat;", 0x0040}, + {"lsqb;", 0x005B}, + {"lback;", 0x005B}, + {"bsol;", 0x005C}, + {"rsqb;", 0x005D}, + {"rbrack;", 0x005D}, + {"Hat;", 0x005E}, + {"lowbar;", 0x005F}, + {"grave;", 0x0060}, + {"DiacriticalGrave;", 0x0060}, + {"lcub;", 0x007B}, + {"lbrace;", 0x007B}, + {"verbar;", 0x007C}, + {"vert;", 0x007C}, + {"VerticalLine;", 0x007C}, + {"rcub;", 0x007D}, + {"rbrace;", 0x007D}, + {"apos;", 0x0027}, + {"OElig;", 0x0152}, + {"oelig;", 0x0153}, + {"Scaron;", 0x0160}, + {"scaron;", 0x0161}, + {"Yuml;", 0x0178}, + {"circ;", 0x02C6}, + {"tilde;", 0x02DC}, + {"ensp;", 0x2002}, + {"emsp;", 0x2003}, + {"emsp13;", 0x2004}, + {"emsp14;", 0x2005}, + {"numsp;", 0x2007}, + {"puncsp;", 0x2008}, + {"thinsp;", 0x2009}, + {"ThinSpace;", 0x2009}, + {"hairsp;", 0x200A}, + {"VeryThinSpace;", 0x200A}, + {"ZeroWidthSpace;", 0x200B}, + {"NegativeVeryThinSpace;", 0x200B}, + {"NegativeThinSpace;", 0x200B}, + {"NegativeMediumSpace;", 0x200B}, + {"NegativeThickSpace;", 0x200B}, + {"zwnj;", 0x200C}, + {"zwj;", 0x200D}, + {"lrm;", 0x200E}, + {"rlm;", 0x200F}, + {"hyphen;", 0x2010}, + {"dash;", 0x2010}, + {"ndash;", 0x2013}, + {"mdash;", 0x2014}, + {"horbar;", 0x2015}, + {"Verbar;", 0x2016}, + {"Vert;", 0x2016}, + {"lsquo;", 0x2018}, + {"OpenCurlyQuote;", 0x2018}, + {"rsquo;", 0x2019}, + {"rsquor;", 0x2019}, + {"CloseCurlyQuote;", 0x2019}, + {"lsquor;", 0x201A}, + {"sbquo;", 0x201A}, + {"ldquo;", 0x201C}, + {"OpenCurlyDoubleQuote;", 0x201C}, + {"rdquo;", 0x201D}, + {"rdquor;", 0x201D}, + {"CloseCurlyDoubleQuote;", 0x201D}, + {"ldquor;", 0x201E}, + {"bdquo;", 0x201E}, + {"dagger;", 0x2020}, + {"Dagger;", 0x2021}, + {"permil;", 0x2030}, + {"lsaquo;", 0x2039}, + {"rsaquo;", 0x203A}, + {"euro;", 0x20AC}, + {"fnof;", 0x0192}, + {"Alpha;", 0x0391}, + {"Beta;", 0x0392}, + {"Gamma;", 0x0393}, + {"Delta;", 0x0394}, + {"Epsilon;", 0x0395}, + {"Zeta;", 0x0396}, + {"Eta;", 0x0397}, + {"Theta;", 0x0398}, + {"Iota;", 0x0399}, + {"Kappa;", 0x039A}, + {"Lambda;", 0x039B}, + {"Mu;", 0x039C}, + {"Nu;", 0x039D}, + {"Xi;", 0x039E}, + {"Omicron;", 0x039F}, + {"Pi;", 0x03A0}, + {"Rho;", 0x03A1}, + {"Sigma;", 0x03A3}, + {"Tau;", 0x03A4}, + {"Upsilon;", 0x03A5}, + {"Phi;", 0x03A6}, + {"Chi;", 0x03A7}, + {"Psi;", 0x03A8}, + {"Omega;", 0x03A9}, + {"alpha;", 0x03B1}, + {"beta;", 0x03B2}, + {"gamma;", 0x03B3}, + {"delta;", 0x03B4}, + {"epsilon;", 0x03B5}, + {"zeta;", 0x03B6}, + {"eta;", 0x03B7}, + {"theta;", 0x03B8}, + {"iota;", 0x03B9}, + {"kappa;", 0x03BA}, + {"lambda;", 0x03BB}, + {"mu;", 0x03BC}, + {"nu;", 0x03BD}, + {"xi;", 0x03BE}, + {"omicron;", 0x03BF}, + {"pi;", 0x03C0}, + {"rho;", 0x03C1}, + {"sigmaf;", 0x03C2}, + {"sigma;", 0x03C3}, + {"tau;", 0x03C4}, + {"upsilon;", 0x03C5}, + {"phi;", 0x03C6}, + {"chi;", 0x03C7}, + {"psi;", 0x03C8}, + {"omega;", 0x03C9}, + {"thetasym;", 0x03D1}, + {"upsih;", 0x03D2}, + {"piv;", 0x03D6}, + {"bull;", 0x2022}, + {"hellip;", 0x2026}, + {"prime;", 0x2032}, + {"Prime;", 0x2033}, + {"oline;", 0x203E}, + {"frasl;", 0x2044}, + {"MediumSpace;", 0x205F}, + {"NoBreak;", 0x2060}, + {"ApplyFunction;", 2061}, + {"af;", 2061}, + {"it;", 0x2062}, + {"InvisibleTimes;", 0x2062}, + {"ic;", 0x2063}, + {"InvisibleComma;", 0x2063}, + {"weierp;", 0x2118}, + {"image;", 0x2111}, + {"real;", 0x211C}, + {"trade;", 0x2122}, + {"alefsym;", 0x2135}, + {"larr;", 0x2190}, + {"uarr;", 0x2191}, + {"rarr;", 0x2192}, + {"darr;", 0x2193}, + {"harr;", 0x2194}, + {"crarr;", 0x21B5}, + {"lArr;", 0x21D0}, + {"uArr;", 0x21D1}, + {"rArr;", 0x21D2}, + {"dArr;", 0x21D3}, + {"hArr;", 0x21D4}, + {"forall;", 0x2200}, + {"part;", 0x2202}, + {"exist;", 0x2203}, + {"empty;", 0x2205}, + {"nabla;", 0x2207}, + {"isin;", 0x2208}, + {"notin;", 0x2209}, + {"ni;", 0x220B}, + {"prod;", 0x220F}, + {"sum;", 0x2211}, + {"minus;", 0x2212}, + {"lowast;", 0x2217}, + {"radic;", 0x221A}, + {"prop;", 0x221D}, + {"infin;", 0x221E}, + {"ang;", 0x2220}, + {"and;", 0x2227}, + {"or;", 0x2228}, + {"cap;", 0x2229}, + {"cup;", 0x222A}, + {"int;", 0x222B}, + {"there4;", 0x2234}, + {"sim;", 0x223C}, + {"cong;", 0x2245}, + {"asymp;", 0x2248}, + {"ne;", 0x2260}, + {"equiv;", 0x2261}, + {"le;", 0x2264}, + {"ge;", 0x2265}, + {"sub;", 0x2282}, + {"sup;", 0x2283}, + {"nsub;", 0x2284}, + {"sube;", 0x2286}, + {"supe;", 0x2287}, + {"oplus;", 0x2295}, + {"otimes;", 0x2297}, + {"perp;", 0x22A5}, + {"sdot;", 0x22C5}, + {"lceil;", 0x2308}, + {"rceil;", 0x2309}, + {"lfloor;", 0x230A}, + {"rfloor;", 0x230B}, + {"lang;", 0x2329}, + {"rang;", 0x232A}, + {"loz;", 0x25CA}, + {"spades;", 0x2660}, + {"clubs;", 0x2663}, + {"hearts;", 0x2665}, + {"diams;", 0x2666} +}; + +const size_t g_htmlEntitiesCount = sizeof(g_htmlEntities) / sizeof(g_htmlEntities[0]); + +const char* g_htmlTags[] = { + "a", + "abbr", + "acronym", + "address", + "applet", + "embed", + "object", + "area", + "article", + "aside", + "audio", + "b", + "base", + "basefont", + "bdi", + "bdo", + "big", + "blockquote", + "body", + "br", + "button", + "canvas", + "caption", + "center", + "cite", + "code", + "col", + "colgroup", + "datalist", + "dd", + "del", + "details", + "dfn", + "dialog", + "dir", + "ul", + "div", + "dl", + "dt", + "em", + "fieldset", + "figcaption", + "figure", + "font", + "footer", + "form", + "frame", + "frameset", + "h1", + "h6", + "head", + "header", + "hr", + "html", + "i", + "iframe", + "img", + "input", + "ins", + "kbd", + "keygen", + "label", + "legend", + "li", + "link", + "main", + "map", + "mark", + "menu", + "menuitem", + "meta", + "meter", + "nav", + "noframes", + "noscript", + "ol", + "optgroup", + "option", + "output", + "p", + "param", + "pre", + "progress", + "q", + "rp", + "rt", + "ruby", + "s", + "samp", + "script", + "section", + "select", + "small", + "source", + "video", + "span", + "strike", + "strong", + "style", + "sub", + "summary", + "sup", + "table", + "tbody", + "td", + "textarea", + "tfoot", + "th", + "thead", + "time", + "title", + "tr", + "track", + "tt", + "u", + "var", + "wbr", + "event-source", + "math", + "svg", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6" +}; + +const size_t g_htmlTagsCount = sizeof(g_htmlTags) / sizeof(g_htmlTags[0]); + +bool startsWithHtmlTagName(const char* text) { + for (size_t index = 0; index < g_htmlTagsCount; ++index) { + // Return true if text starts with one of html tags + if (my_stristarts_with(text, g_htmlTags[index])) { + // starts with html tag, followed by space/tab/crlf character (see man isspace(), + // or ends with '>' character. + char termChar = text[strlen(g_htmlTags[index])]; + if (isspace(termChar) || termChar == '>' || termChar == '/') { + return true; + } + } + } + + return false; +} + +string normalize_uri(const string& uri) { + string result; + string::const_iterator mark = uri.begin(); + bool isNumeric = false; + + string::const_iterator it = uri.begin(); + for (; it != uri.end() && *it != '?'; ++it) { + if (*it == '/') { + if (mark != it) { + if (isNumeric) { + result += "_num"; + } + else { + result.append(mark, it); + } + } + + result += "/"; + mark = it + 1; + isNumeric = true; + continue; + } + + // reset isNumeric flag on first non-digit character in the path element string + if (!isdigit(*it)) { + isNumeric = false; + } + } + + // At this point, "it" points to where scanning stopped (can be end of uri string or the '?' character) + // Append the rest of the string (or "_num" if last uri part was all numeric) - to the output. + if (mark != it) { + if (isNumeric) { + result += "_num"; + } + else { + result.append(mark, it); + } + } + + return result; +} + +string +normalize_param(const string& param) +{ + string result; + string::const_iterator mark = param.begin(); + bool isNumeric = true; + bool isHex = true; + + string::const_iterator it = param.begin(); + for (; it != param.end(); ++it) { + if (!isalnum(*it)) { + if (mark != it) { + if (isNumeric || (isHex && it - mark >= MIN_HEX_LENGTH)) { + result += "_num"; + } + else { + result.append(mark, it); + } + } + + result += *it; + mark = it + 1; + isNumeric = true; + isHex = true; + continue; + } + + // reset isNumeric flag on first non-digit character in the path element string + if (isHex && !isdigit(*it)) { + if (!isHexDigit(*it)) { + isHex = false; + } + isNumeric = false; + } + } + + // At this point, "it" points to where scanning stopped (can be end of uri string or the '?' character) + // Append the rest of the string (or "_num" if last uri part was all numeric) - to the output. + if (mark != it) { + if (isNumeric || (isHex && it - mark >= MIN_HEX_LENGTH)) { + result += "_num"; + } + else { + result.append(mark, it); + } + } + + return result; +} + +void unescapeUnicode(string& text) { + string::iterator it = text.begin(); + string::iterator result = it; + char acc[16]; // accumulates characters we are parsing and do not want to copy directly. + // max len really possible is "\u00000000" + 1 char = 11 chars + char* pAcc = NULL; // when non-NULL, points where to put next character inside acc buffer + int digitsAnticipated = 0; // in state STATE_ESCAPE, how many hex digits we anticipate to be parsed + uint32_t code = 0; // The Unicode codepoint value can't be larger than 32 bits + char* p; + // in state STATE_ESCAPE_X, how many non-zerohex digits discovered - to eliminate leading zeroes like \x000012 + int nonZeroHexCounter = 0; + enum { + STATE_COPY, + STATE_FLUSH, + STATE_ESCAPE, + STATE_ESCAPE_U, + STATE_ESCAPE_X + } state = STATE_COPY; + + for (; it != text.end(); ++it) { + const char ch = *it; + + switch (state) { + case STATE_FLUSH: { + // flush any accumulated left-overs into output buffer + if (pAcc) { + for (p = acc; p < pAcc; p++) { + *result++ = *p; + } + pAcc = NULL; // clear the acc buffer after we flushed it + } + state = STATE_COPY; + // fall-through + //RB: why no break? + } + // fallthrough + case STATE_COPY: { + + if (ch == '\\') { + // start accumulating characters instead of copying them + pAcc = acc; + state = STATE_ESCAPE; + break; + } + break; + } + case STATE_ESCAPE: { + // decide which kind of escape + if (ch == 'u') { + digitsAnticipated = 4; // parse/skip 4 hex digits + code = 0; + state = STATE_ESCAPE_U; + } + else if (ch == 'U') { + digitsAnticipated = 8; // parse/skip 8 hex digits + code = 0; + state = STATE_ESCAPE_U; + } + else if (ch == 'x') { +#if 1 + digitsAnticipated = 1; // anticipate at least one HEX digit after \x + code = 0; + nonZeroHexCounter = 0; + state = STATE_ESCAPE_X; +#else + digitsAnticipated = 2; // parse/skip 2 hex digits + code = 0; + state = STATE_ESCAPE_U; +#endif + } + else { + // this is invalid escape sequence: rollback and copy this character too + state = STATE_FLUSH; + } + break; + } + case STATE_ESCAPE_U: { + if (isHexDigit(ch)) { + // accumulate code value + code = (code << 4) + (isdigit(ch) ? ch - '0' : tolower(ch) - 'a' + 10); + digitsAnticipated--; + + if (digitsAnticipated == 0) { + // only output ASCII codes <= 127. "swallow" all unicode. + if (code <= 127) { + *result++ = (char)code; + } + else if (isSpecialUnicode(code)) { + *result++ = convertSpecialUnicode(code); + } + + if (pAcc) { + pAcc = NULL; // throw away the accumulated source (escaped) sequencec. + } + + // not STATE_COPY to avoid outputting current ch verbatim. + // FLUSH will output nothing because there's no ACC + state = STATE_FLUSH; + break; + } + + } + else { + // invalid (non-hex) digit enountered + state = STATE_FLUSH; + } + + break; + } + case STATE_ESCAPE_X: { + if (isHexDigit(ch)) { + if ((nonZeroHexCounter) > 1) { + *result++ = (char)code; + if (pAcc) { + pAcc = NULL; // throw away the accumulated source (escaped) sequence. + } + state = STATE_COPY; + } else { + code = (code << 4) + (isdigit(ch) ? ch - '0' : tolower(ch) - 'a' + 10); + code &= 0xFF; // clamp the code value to two last digits + // Once at least one valid hex digit is here the sequence is considered + // valid and there's no need to accumulate it anymore. + if (pAcc) { + pAcc = NULL; + } + + if (digitsAnticipated > 0) { + digitsAnticipated--; + } + if (code) { + nonZeroHexCounter++; + } + } + } else { + // According to C standard, '\x' sequence must be followed by at least 1 valid hex digit + if (digitsAnticipated > 0) { + // This is first character right after the '\x' sequence, + // and it is not a valid hex. This is bad sequence. + state = STATE_FLUSH; + } else { + // We found non-hex character that terminates our \xhhhhh... sequence + *result++ = (char)code; + if (pAcc) { + pAcc = NULL; // throw away the accumulated source (escaped) sequence. + } + + if (ch == '\\') { + // start accumulating characters instead of copying them + pAcc = acc; + state = STATE_ESCAPE; + break; + } + + // STATE_COPY will cause current character (sequence terminator) + // to be output verbatim. + state = STATE_COPY; + } + } + + if (digitsAnticipated > 0) { + digitsAnticipated--; + } + break; + } + } + + // Copy to output + if (state == STATE_COPY) { + *result++ = ch; + } + + // Accumulate + if (pAcc) { + // Ensure we don't have buffer overflow + assert(size_t(pAcc - acc) < sizeof(acc)); + *pAcc++ = ch; + } + } + + dbgTrace(D_WAAP) << " - LOOP FINISHED with state=" << state << "; digitsAnticipated=" << + digitsAnticipated << ", acc='" << string(acc, pAcc ? (int)(pAcc - acc) : 0) << "'"; + + // Output code if we just finished decoding an escape sequence succesully and reached end of string + if (state == STATE_ESCAPE_U && digitsAnticipated == 0) { + // only output ASCII codes <= 127. "swallow" all unicode. + if (code <= 127) { + *result++ = (char)code; + } + else if (isSpecialUnicode(code)) { + *result++ = convertSpecialUnicode(code); + } + + if (pAcc) { + pAcc = NULL; // throw away the accumulated source (escaped) sequencec. + } + } + else if (state == STATE_ESCAPE_X) { + if (isSpecialUnicode(code)) { + *result++ = convertSpecialUnicode(code); + } + else + { + *result++ = (char)code; + } + } + + // flush any accumulated left-overs into output buffer + if (pAcc) { + for (p = acc; p < pAcc; p++) { + *result++ = *p; + } + } + + text.erase(result, text.end()); +} + +// Attempts to validate and decode utf7-encoded chunk. +// Returns "next" iterator to position where to continue parsing for next chunks, and +// fills the "decoded" string with decoded data. +// If failed, the "next" will be equal to passed "it", and empty string put in "decoded". +inline const string::const_iterator +decodeUTF7Chunk(string::const_iterator it, string::const_iterator end, string& decoded) { + decoded.clear(); + unsigned char val = 0; + uint32_t acc = 0; + int acc_bits = 0; // how many bits are filled in acc + string::const_iterator next = it; + + while (it != end) { + unsigned char c = *it; + + if (c >= 'A' && c <= 'Z') { + val = c - 'A'; + } + else if (c >= 'a' && c <= 'z') { + val = c - 'a' + 26; + } + else if (c >= '0' && c <= '9') { + val = c - '0' + 52; + } + else if (c == '+') { + val = 62; + } + else if (c == '/') { + val = 63; + } + else if (c == '-') { + // end of encoded sequence (the '-' itself must not be output) + if (!decoded.empty()) { + next = it; + return next; // successfully decoded. Returns decoded data in "decoded" parameter + } + + decoded.clear(); // discard partial data + return next; + } + else { + decoded.clear(); // discard partial data + return next; + } + + acc = acc << 6 | val; + acc_bits += 6; + + if (acc_bits >= 16) { // we got 16 bits or more in the accumulator + int code = (acc >> (acc_bits - 16)) & 0xFFFF; + + // Take into account still-printable Unicode characters, convert them back to ASCII + if (isSpecialUnicode(code)) { + code = convertSpecialUnicode(code); + } + + // Stop and return empty if we hit at least one non-printable character + if (!isprint(code) && code != 0) { + decoded.clear(); // discard partial data + return next; + } + + decoded += (char)code; + acc_bits -= 16; + acc &= (1 - (1 << acc_bits)); // leave only acc_bits low bits in the acc, clear the rest. + } + + it++; + } + + decoded.clear(); // discard partial data + return next; +} + +string filterUTF7(const string& text) { + string result; + string decoded; + decoded.reserve(8); + result.reserve(text.length()); + + for (string::const_iterator it = text.begin(); it != text.end(); ++it) { + if (*it == '+') { + if (it + 1 == text.end()) { // "+" at end of string + result += *it; + } + else if (*(it + 1) == '-') { + // '+-' combination is converted to single '+' + result += '+'; + it++; // this skips the "-" + if (it == text.end()) { + break; + } + } + else { + // attempt to decode chunk + it = decodeUTF7Chunk(it + 1, text.end(), decoded); + if (decoded.empty()) { // decoding failed + result += '+'; + result += *it; + } + else { // decoding succeeded + result += decoded; + } + } + } + else { + result += *it; + } + } + + return result; +} + +// Attempts to validate and decode base64-encoded chunk. +// Value is the full value inside which potential base64-encoded chunk was found, +// it and end point to start and end of that chunk. +// Returns true if succesful (and fills the "decoded" string with decoded data). +// Success criterias: +// 0. encoded sequence covers the whole value (doesn't have any characters around it) +// 1. encoded sequence consist of base64 alphabet (may end with zero, one or two '=' characters') +// 2. length of encoded sequence is exactly divisible by 4. +// 3. length of decoded is minimum 5 characters. +// 4. percent of non-printable characters (!isprint()) +// in decoded data is less than 10% (statistical garbage detection). +// Returns false above checks fail. +bool +b64DecodeChunk( + const string& value, + string::const_iterator it, + string::const_iterator end, + string& decoded) +{ + decoded.clear(); + uint32_t acc = 0; + int acc_bits = 0; // how many bits are filled in acc + int terminatorCharsSeen = 0; // whether '=' character was seen, and how many of them. + uint32_t nonPrintableCharsCount = 0; + + dbgTrace(D_WAAP) << "b64DecodeChunk: value='" << value << "' match='" << string(it, end) << "'"; + + // skip "base64," prefix if the line is starting with it. + if (end - it >= 7 && + *it == 'b' && + *(it + 1) == 'a' && + *(it + 2) == 's' && + string(it, it + 7) == "base64,") { + it += 7; // skip the prefix + } + else { + // If the base64 candidate match within value is surrounded by other dat + // (doesn't cover the value fully) - ignore the match. + // This will result in the match being scanned raw. + // Note that this purposedly doesn't include matches starting with "base64," + // prefix: we do want those prefixed matches to be decoded! + if (it != value.begin() || end != value.end()) { + dbgTrace(D_WAAP) << "b64DecodeChunk: (leave as-is) because match is surrounded by other data."; + return false; + } + } + + // The encoded data length (without the "base64," prefix) should be exactly divisible by 4 + // to be considered valid base64. + if ((end - it) % 4 != 0) { + dbgTrace(D_WAAP) << + "b64DecodeChunk: (leave as-is) because encoded data length should be exactly divisible by 4."; + return false; + } + + while (it != end) { + unsigned char c = *it; + + if (terminatorCharsSeen) { + // terminator characters must all be '=', until end of match. + if (c != '=') { + dbgTrace(D_WAAP) << + "b64DecodeChunk: (leave as-is) because terminator characters must all be '=', until end of match."; + return false; + } + + // We should see 0, 1 or 2 (no more) terminator characters + terminatorCharsSeen++; + + if (terminatorCharsSeen > 2) { + dbgTrace(D_WAAP) << "b64DecodeChunk: (leave as-is) because terminatorCharsSeen > 2"; + return false; + } + + // allow for more terminator characters + it++; + continue; + } + + unsigned char val = 0; + + if (c >= 'A' && c <= 'Z') { + val = c - 'A'; + } + else if (c >= 'a' && c <= 'z') { + val = c - 'a' + 26; + } + else if (isdigit(c)) { + val = c - '0' + 52; + } + else if (c == '+') { + val = 62; + } + else if (c == '/') { + val = 63; + } + else if (c == '=') { + // Start tracking terminator characters + terminatorCharsSeen++; + it++; + continue; + } + else { + dbgTrace(D_WAAP) << "b64DecodeChunk: (leave as-is) because of non-base64 character ('" << c << + "', ASCII " << (unsigned int)c << ")"; + return false; // non-base64 character + } + + acc = (acc << 6) | val; + acc_bits += 6; + + if (acc_bits >= 8) { + int code = (acc >> (acc_bits - 8)) & 0xFF; + // only leave low "acc_bits-8" bits, clear all higher bits + uint32_t mask = ~(1 << (acc_bits - 8)); + acc &= mask; + acc_bits -= 8; + + // Count non-printable characters seen + if (!isprint(code)) { + nonPrintableCharsCount++; + } + + decoded += (char)code; + } + + it++; + } + + // end of encoded sequence decoded. + + dbgTrace(D_WAAP) << "b64DecodeChunk: decoded.size=" << decoded.size() << ", nonPrintableCharsCount=" << + nonPrintableCharsCount << "; decoded='" << decoded << "'"; + + // Return success only if decoded.size>=5 and there are less than 10% of non-printable + // characters in output. + if (decoded.size() >= 5) { + if (nonPrintableCharsCount * 10 < decoded.size()) { + dbgTrace(D_WAAP) << "b64DecodeChunk: (decode/replace) decoded.size=" << decoded.size() << + ", nonPrintableCharsCount=" << nonPrintableCharsCount << ": replacing with decoded data"; + } + else { + dbgTrace(D_WAAP) << "b64DecodeChunk: (delete) because decoded.size=" << decoded.size() << + ", nonPrintableCharsCount=" << nonPrintableCharsCount; + // If percentage of non-printable characters in decoded is high, filter them out to prevent false alarms. + decoded.clear(); + } + return true; // successfully decoded. Returns decoded data in "decoded" parameter + } + + // If decoded size is too small - leave the encoded value (return false) + decoded.clear(); // discard partial data + dbgTrace(D_WAAP) << "b64DecodeChunk: (leave as-is) because decoded too small. decoded.size=" << decoded.size() << + ", nonPrintableCharsCount=" << nonPrintableCharsCount; + return false; +} + +vector split(const string& s, char delim) { + vector elems; + stringstream ss(s); + string value; + while (getline(ss, value, delim)) { + elems.push_back(Waap::Util::trim(value)); + } + return elems; +} + +namespace Waap { +namespace Util { + +#define B64_TRAILERCHAR '=' +static const string b64_prefix("base64,"); +static bool err = false; + +static const SingleRegex invalid_hex_evasion_re( + "%([g-zG-Z][0-9a-zA-Z]|[0-9a-zA-Z][g-zG-Z])", + err, + "invalid_hex_evasion" +); +static const SingleRegex broken_utf_evasion_re( + "(?:^|[^%])(%[0-9a-f]%[0-9a-f])", + err, + "broken_utf_evasion" +); + +static void b64TestChunk(const string &s, + string::const_iterator chunkStart, + string::const_iterator chunkEnd, + RegexSubCallback_f cb, + int &decodedCount, + int &deletedCount, + string &outStr) +{ + size_t chunkLen = (chunkEnd - chunkStart); + + if ((chunkEnd - chunkStart) > static_cast(b64_prefix.size()) && + chunkStart[0] == 'b' && chunkStart[1] == 'a' && chunkStart[2] == 's' && chunkStart[3] == 'e' && + chunkStart[4] == '6' && chunkStart[5] == '4' && chunkStart[6] == ',') { + chunkLen -= b64_prefix.size(); + } + + size_t chunkRem = chunkLen % 4; + + // Only match chunk whose length is divisible by 4 + string repl; + if (chunkRem == 0 && cb(s, chunkStart, chunkEnd, repl)) { + // Succesfully matched b64 chunk + if (!repl.empty()) { + outStr += repl; + decodedCount++; + } + else { + deletedCount++; + } + } + else { + // Chunk was not processed - put original text + size_t from = chunkStart - s.begin(); + size_t len = chunkEnd - chunkStart; + outStr += s.substr(from, len); + } +} + +void b64Decode( + const string &s, + RegexSubCallback_f cb, + int &decodedCount, + int &deletedCount, + string &outStr) +{ + decodedCount = 0; + deletedCount = 0; + outStr = ""; + int offsetFix = 0; + + string::const_iterator it = s.begin(); + + // Minimal length + if (s.end() - it < 8) { + return; + } + + // Search for substrings that match these criterias: + // 1. substring length is divisible by 4 + // 2. substring contains only letters a-z, 0-9, '/' or '+' except last 1 or two characters that can be '=' + + string::const_iterator chunkStart = s.end(); + for (; it != s.end(); ++it) { + bool isB64AlphaChar = Waap::Util::isAlphaAsciiFast(*it) || isdigit(*it) || *it=='/' || *it=='+'; + if (chunkStart == s.end()) { + if (isB64AlphaChar) { + // start tracking potential b64 chunk + chunkStart = it; + } + else { + // Add anything before the potential match + outStr += string(1, *it); + } + } + else { + // tracking b64 chunk + if (!isB64AlphaChar) { + if (*it == ',') { + // Check back and skip the "base64," prefix + if (chunkStart + b64_prefix.size() - 1 == it) { + string cand(chunkStart, it + 1); + if (cand == b64_prefix) { + offsetFix = b64_prefix.size(); + continue; + } + } + } + + size_t chunkLen = (it - chunkStart) - offsetFix; + size_t chunkRem = chunkLen % 4; + + // Allow only one or two '=' characters at the end of the match + if ((*it == B64_TRAILERCHAR) && (chunkRem == 2 || chunkRem == 3)) { + continue; + } + + // Decode and add chunk + b64TestChunk(s, chunkStart, it, cb, decodedCount, deletedCount, outStr); + + // stop tracking b64 chunk + outStr += string(1, *it); // put the character that terminated the chunk + chunkStart = s.end(); + offsetFix = 0; + } + } + } + + if (chunkStart != s.end()) { + b64TestChunk(s, chunkStart, it, cb, decodedCount, deletedCount, outStr); + } +} + +// Base64 functions stolen from orchestration_tools.cc +static const string base64_base_str = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + +string +base64Encode(const string &input) +{ + string out; + int val = 0, val_base = -6; + for (unsigned char c : input) { + val = (val << 8) + c; + val_base += 8; + while (val_base >= 0) { + out.push_back(base64_base_str[(val >> val_base) & 0x3F]); + val_base -= 6; + } + } + // -6 indicates the number of bits to take from each character + // (6 bits is enough to present a range of 0 to 63) + if (val_base > -6) out.push_back(base64_base_str[((val << 8) >> (val_base + 8)) & 0x3F]); + while (out.size() % 4) out.push_back('='); + return out; +} + +bool find_in_map_of_stringlists_keys(const string &what, const map_of_stringlists_t &where) +{ + for (map_of_stringlists_t::const_iterator it = where.begin(); it != where.end(); ++it) { + if (it->first.find(what) != string::npos) { + return true; + } + } + + return false; +} + +void remove_in_map_of_stringlists_keys(const string &what, map_of_stringlists_t &where) +{ + map_of_stringlists_t::iterator it = where.begin(); + + while (it != where.end()) { + if (it->first.find(what) != string::npos) { + it = where.erase(it); + } + else { + it++; + } + } +} + +void remove_startswith(vector &vec, const string &prefix) +{ + vec.erase( + remove_if(vec.begin(), vec.end(), + [&prefix](const string &kw) + { + return boost::starts_with(kw, prefix); + } + ), + vec.end() + ); +} + +string AES128Decrypt( + string& key, + string& iv, + string& message) +{ + unsigned char* outdata = new unsigned char[message.length()]; + + // data structure that contains the key itself + AES_KEY aes_key; + + // set the encryption key + AES_set_decrypt_key((const unsigned char*)key.c_str(), 128, &aes_key); + + AES_cbc_encrypt( + (unsigned char*)message.c_str(), + outdata, message.length(), + &aes_key, (unsigned char*)iv.c_str(), + AES_DECRYPT + ); + + // get value without padding + size_t padding = outdata[message.length() - 1]; // last byte contain padding size + string result = string((const char*)outdata, message.length() - padding); + + delete[] outdata; + return result; +} + +string +base64Decode(const string &input) +{ + string out; + vector T(256, -1); + for (int i = 0; i < 64; i++) { + T[base64_base_str[i]] = i; + } + int val = 0, valb = -8; + for (unsigned char c : input) { + if (T[c] == -1) break; + val = (val << 6) + T[c]; + valb += 6; + if (valb >= 0) { + out.push_back(char((val >> valb) & 0xFF)); + valb -= 8; + } + } + return out; +} + +bool +containsInvalidUtf8(const string &payload) +{ + return invalid_hex_evasion_re.hasMatch(payload); +} + +string +unescapeInvalidUtf8(const string &payload) +{ + dbgFlow(D_WAAP_EVASIONS); + vector regex_matches; + invalid_hex_evasion_re.findMatchRanges(payload, regex_matches); + + string unescaped_text = payload; + for (const auto &match : regex_matches) { + static const int evasion_pattern_length = 3; + + int num = 0; + size_t pos = match.start + 1; + for (; pos < match.end; pos++) { + const char &byte = unescaped_text[pos]; + if (isdigit(byte)) { + num = (num << 4) + charToDigit(byte); + } else { + num = (num << 4) + ((tolower(byte) - 'a') + 10); + } + } + + char buf[evasion_pattern_length]; + snprintf(buf, evasion_pattern_length, "%02x", (num & 0xff)); + unescaped_text.replace(match.start + 1, evasion_pattern_length - 1, buf); + + dbgTrace(D_WAAP_EVASIONS) << "Value after conversion: decimal = " << num << ", hex = " << buf; + } + + dbgTrace(D_WAAP_EVASIONS) << "unescaped_text: " << unescaped_text; + + return unescaped_text; +} + +bool +containsBrokenUtf8(const string &payload) +{ + return broken_utf_evasion_re.hasMatch(payload); +} + +string +unescapeBrokenUtf8(const string &payload) +{ + string unescaped_text; + unescaped_text.reserve(payload.length()); + + int prev_esc_pos = -1; + for (size_t pos = 0; pos < payload.length(); ++pos) { + char c = payload[pos]; + if (c == '%') { + // skip copying current '%' when encountered with the 2nd '%' + // that follows and followed by only one hex digit + if (prev_esc_pos >= 0 && pos-prev_esc_pos == 2 && isxdigit(payload[pos-1]) && + pos+1 < payload.length() && isxdigit(payload[pos+1]) ) { + prev_esc_pos = -1; + continue; + } + // mark current '%' only when not following another '%' + if (prev_esc_pos < 0 || pos-prev_esc_pos > 1) { + prev_esc_pos = pos; + } + } + unescaped_text += c; + } + + dbgTrace(D_WAAP_EVASIONS) << "unescaped_text: " << unescaped_text; + return unescaped_text; +} + +string +charToString(const char* s, int slen) +{ + if (!s || slen == 0) return ""; + + return string(s, slen); +} + +string +vecToString(const vector& vec, char delim) { + ostringstream vts; + + string delimStr; + delimStr.push_back(delim); + if (delim != '\n') + { + delimStr.push_back(' '); + } + + vts << "["; + if (!vec.empty()) + { + // Convert all but the last element to avoid a trailing "," + copy(vec.begin(), vec.end() - 1, + ostream_iterator(vts, delimStr.c_str())); + + // Now add the last element with no delimiter + vts << vec.back(); + } + else + { + return string(); + } + vts << "]"; + + + return vts.str(); +} + + +string +obfuscateXor(const string& toEncrypt) { + char key[] = "CHECKPOINT"; //Any chars will work + string output = toEncrypt; + + for (size_t i = 0; i < toEncrypt.size(); i++) { + output[i] = toEncrypt[i] ^ key[i % ((sizeof(key)-1) / sizeof(char))]; + } + + return output; +} + +string +obfuscateXorBase64(const string& toEncrypt) { + return base64Encode(obfuscateXor(toEncrypt)); +} + +string injectSpacesToString(const string& str) { + string retStr = ""; + if (str.length() == 0) + { + return retStr; + } + retStr.resize(str.length() * 2, ' '); + for (size_t i = 0; i < str.length(); i++) + { + retStr[i * 2] = str[i]; + } + retStr.pop_back(); + return retStr; +} + +ReportIS::Severity computeSeverityFromThreatLevel(ThreatLevel threatLevel) { + if (threatLevel == NO_THREAT) { + return ReportIS::Severity::INFO; + } + else if (threatLevel == THREAT_INFO) { + return ReportIS::Severity::LOW; + } + else if (threatLevel == LOW_THREAT) { + return ReportIS::Severity::MEDIUM; + } + else if (threatLevel == MEDIUM_THREAT) { + return ReportIS::Severity::HIGH; + } + + return ReportIS::Severity::CRITICAL; +} + +ReportIS::Priority computePriorityFromThreatLevel(ThreatLevel threatLevel) { + if (threatLevel == NO_THREAT) { + return ReportIS::Priority::LOW; + } + else if (threatLevel == THREAT_INFO) { + return ReportIS::Priority::MEDIUM; + } + else if (threatLevel == LOW_THREAT) { + return ReportIS::Priority::MEDIUM; + } + else if (threatLevel == MEDIUM_THREAT) { + return ReportIS::Priority::HIGH; + } + + return ReportIS::Priority::HIGH; +} + +string computeConfidenceFromThreatLevel(ThreatLevel threatLevel) +{ + switch(threatLevel) { + case NO_THREAT: return "Low"; + case THREAT_INFO: return "Low"; + case LOW_THREAT: return "Medium"; + case MEDIUM_THREAT: return "High"; + case HIGH_THREAT: return "Very High"; + } + dbgWarning(D_WAAP) << "Reached impossible threat level value of: " << static_cast(threatLevel); + return "Low"; +} + +void decodePercentEncoding(string &text, bool decodePlus) +{ + // Replace %xx sequences by their single-character equivalents. + // Do not replace the '+' symbol by space character because this would corrupt some base64 source strings + // (base64 alphabet includes the '+' character). + text.erase( + unquote_plus(text.begin(), text.end(), checkUrlEncoded(text.data(), text.size()), decodePlus), text.end() + ); + dbgTrace(D_WAAP) << "decodePercentEncoding: (after unquote_plus) '" << text << "'"; +} + +// Try to detect/decode UTF16 (detecting either BE and LE variant). +// The function uses statistics to try to guess whether UTF-16 is present and its exact variant (Big/Little endianess) +// If UTF-16 value is detected, value in cur_val is converted to utf8 in-place for use in later processing. +void decodeUtf16Value(const ValueStatsAnalyzer &valueStats, string &cur_val) +{ + // Do not change cur_val if UTF16 is not detected + if (!valueStats.isUTF16) { + return; + } + + dbgTrace(D_WAAP) << "decoding UTF-16 into UTF-8 in-place"; + + bool isBigEndian = false; + size_t pos = 0; + + // First, detect BOM as a hint of UTF16-BE vs. LE variant. See https://unicode.org/faq/utf_bom.html#utf8-4 + if (cur_val[0] == (char)0xFE && cur_val[1] == (char)0xFF) { + // UTF16-BE hint + isBigEndian = true; + // Skip the BOM + pos++; + } + else if (cur_val[0] == (char)0xFF && cur_val[1] == (char)0xFE) { + // UTF16-LE hint + isBigEndian = false; + // Skip the BOM + pos++; + } + else { + isBigEndian = (valueStats.longestZerosSeq[0] > valueStats.longestZerosSeq[1]); + } + + // Decode utf16 into utf8 + string utf8Out; + for (; pos> 6) | 0xC0; + utf8Out += (code & 0x3F) | 0x80; + } + else { // (code <= 0xFFFF : always true because code type is unsigned short which is 16-bit + utf8Out += (code >> 12) | 0xE0; + utf8Out += ((code >> 6) & 0x3F) | 0x80; + utf8Out += (code & 0x3F) | 0x80; + } + } + + // Return the value converted from UTF-16 to UTF-8 + cur_val = utf8Out; +} + +bool testUrlBareUtf8Evasion(const string &line) { + size_t percentPos = 0; + + while (percentPos < line.size()) { + percentPos = line.find("%", percentPos); + + if (percentPos == string::npos) { + return false; + } + + if (percentPos + 2 < line.size() && tolower(line[percentPos + 1]) == 'c' && line[percentPos + 2] == '0') { + // found "%c0" + return true; + } + + // Continue searching from next character after '%' + percentPos++; + } + + return false; +} + +bool testUrlBadUtf8Evasion(const string &line) { + size_t percentPos = 0; + + while (percentPos < line.size()) { + percentPos = line.find("%", percentPos); + + if (percentPos == string::npos) { + return false; + } + + if (percentPos + 2 < line.size() && tolower(line[percentPos + 1]) == 'c' && line[percentPos + 2] == '1') { + // found "%c1" + return true; + } + + // Continue searching from next character after '%' + percentPos++; + } + + return false; +} + +string urlDecode(string src) { + src.erase(unquote_plus(src.begin(), src.end(), true, false), src.end()); + return src; +} + +// LCOV_EXCL_START Reason: The function will be deleted on another task +string +stripOptionalPort(const string::const_iterator &first, const string::const_iterator &last) +{ + // Microsoft XFF+IPv6+Port yikes - see also here https://github.com/eclipse/jetty.project/issues/3630 + if (*first == '[') { + // Possible bracketed IPv6 address such as "[2001:db8::1]" + optional numeric ":" + auto close_bracket = find(first + 1, last, ']'); + if (close_bracket == last) return string(first, last); + return string(first+1, close_bracket); + } + + auto first_colon = find(first, last, ':'); + if (first_colon == last) return string(first, last); + + // If there is more than one colon it means its probably IPv6 address without brackets + auto second_colon = find(first_colon + 1, last, ':'); + if (second_colon != last) return string(first, last); + + // If there's only one colon it can't be IPv6 and can only be IPv4 with port + return string(first, first_colon); +} + +bool +isIpTrusted(const string &ip, const vector &trusted_ips) +{ + Waap::Util::CIDRData cidr_data; + for (const auto &trusted_ip : trusted_ips) { + if ( + ip == trusted_ip || + (Waap::Util::isCIDR(trusted_ip, cidr_data) && Waap::Util::cidrMatch(ip, cidr_data)) + ) { + return true; + } + } + return false; +} + +string extractForwardedIp(const string &x_forwarded_hdr_val) +{ + vector xff_splitted = split(x_forwarded_hdr_val, ','); + vector trusted_ips; + string forward_ip; + + auto identify_config = getConfiguration( + "rulebase", + "usersIdentifiers" + ); + + if (!identify_config.ok()) { + dbgDebug(D_WAAP) << "did not find xff definition in policy"; + } else { + trusted_ips = (*identify_config).getHeaderValuesFromConfig("x-forwarded-for"); + } + + if (xff_splitted.size() > 0) + { + for (size_t k = 0; k < xff_splitted.size(); ++k) + { + string optional_result = trim(xff_splitted[k]); + optional_result = stripOptionalPort(optional_result.cbegin(), optional_result.cend()); + if (isIpAddress(optional_result)) + { + if (!isIpTrusted(optional_result, trusted_ips) && !trusted_ips.empty()) { + return ""; + } else if (forward_ip.empty()) { + forward_ip = optional_result; + } + } + } + } + return forward_ip; +} + +bool isIpAddress(const string &ip_address) +{ + struct in_addr source_inaddr; + struct in6_addr source_inaddr6; + + // check from which type the target ip and check if ip belongs to is mask ip + //convert sourceip to ip v4 or v6. + bool isIpV4 = inet_pton(AF_INET, ip_address.c_str(), &source_inaddr) == 1; + bool isIpV6 = inet_pton(AF_INET6, ip_address.c_str(), &source_inaddr6) == 1; + + return isIpV4 || isIpV6; +} + +// LCOV_EXCL_STOP + +string extractKeyValueFromCookie(const string &cookie, const string &key) +{ + string source = ""; + vector cookie_splitted = split(cookie, ';'); + for (size_t l = 0; l < cookie_splitted.size(); ++l) + { + vector cookie_key_splitted = split(cookie_splitted[l], '='); + if (cookie_key_splitted.empty()) + { + dbgWarning(D_WAAP) << "Failed to split the key-value from: " << cookie_splitted[l]; + continue; + } + if (cookie_key_splitted[0] == key) + { + source = cookie_key_splitted[1]; + + if (key == "_oauth2_proxy") + { + source = Waap::Util::base64Decode(source); + + vector currentUserIdentifier_splitted = split(source, '|'); + + if (currentUserIdentifier_splitted.size() > 0) + { + source = currentUserIdentifier_splitted[0]; + } + } + break; + } + } + dbgTrace(D_WAAP) << "extracted source from Cookie:" << key << " : " << source; + return source; +} + +bool vectorStringContain(const vector& vec, const string& str) +{ + for(auto ¶m : vec) { + if(param.compare(str) == 0) + { + return true; + } + } + return false; +} + +ContentType detectContentType(const char* hdr_value) { + const char* plus_p = ::strrchr(hdr_value, '+'); + // Detect XML content type if Content-Type header value ends with "+xml". + // For example: "application/xhtml+xml", or "image/svg+xml" + // For reference: see first answer here: + // https://stackoverflow.com/questions/2965587/valid-content-type-for-xml-html-and-xhtml-documents + if (plus_p && my_stricmp(plus_p + 1, "xml")) { + return CONTENT_TYPE_XML; + } + + const char* slash_p = ::strrchr(hdr_value, '/'); + + if (slash_p) { + // Detect XML content type if Content-Type header value ends with "/xml" + if (my_stricmp(slash_p + 1, "xml")) { + return CONTENT_TYPE_XML; + } + + // Detect JSON content type if Content-Type header value is application/json or ends with "/json" + if (my_stricmp(slash_p + 1, "json") || my_stristarts_with(hdr_value, "application/json")) { + return CONTENT_TYPE_JSON; + } + + // Detect HTML content type + if (my_stristarts_with(hdr_value, "text/html")) { + return CONTENT_TYPE_HTML; + } + + // Detect Multiplart Form Data content type + if (my_stristarts_with(hdr_value, "multipart/form-data")) { + return CONTENT_TYPE_MULTIPART_FORM; + } + + // Detect URL Encoded content type + if (my_stristarts_with(hdr_value, "application/x-www-form-urlencoded")) { + return CONTENT_TYPE_URLENCODED; + } + + // Detect binary xml file type + if (my_stristarts_with(hdr_value, "application/vnd.ms-sync.wbxml")) { + return CONTENT_TYPE_WBXML; + } + } + + return CONTENT_TYPE_UNKNOWN; +} + +string convertParamTypeToStr(ParamType type) +{ + switch (type) + { + case UNKNOWN_PARAM_TYPE: + return "unknown"; + case HTML_PARAM_TYPE: + return "html_input"; + case URL_PARAM_TYPE: + return "urls"; + case FREE_TEXT_PARAM_TYPE: + return "free_text"; + case PIPE_PARAM_TYPE: + return "pipes"; + case LONG_RANDOM_TEXT_PARAM_TYPE: + return "long_random_text"; + case BASE64_PARAM_TYPE: + return "base64"; + case ADMINISTRATOR_CONFIG_PARAM_TYPE: + return "administration_config"; + case FILE_PATH_PARAM_TYPE: + return "local_file_path"; + case SEMICOLON_DELIMITED_PARAM_TYPE: + return "semicolon_delimiter"; + case ASTERISK_DELIMITED_PARAM_TYPE: + return "asterisk_delimiter"; + case COMMA_DELIMITED_PARAM_TYPE: + return "comma_delimiter"; + case AMPERSAND_DELIMITED_PARAM_TYPE: + return "ampersand_delimiter"; + case BINARY_PARAM_TYPE: + return "binary_input"; + default: + dbgWarning(D_WAAP) << "unrecognized type " << to_string(type); + return "unrecognized type"; + } +} + +ParamType convertTypeStrToEnum(const string& typeStr) +{ + static unordered_map sNameTypeMap = { + {"unknown", ParamType::UNKNOWN_PARAM_TYPE}, + {"administration_config", ParamType::ADMINISTRATOR_CONFIG_PARAM_TYPE}, + {"base64", ParamType::BASE64_PARAM_TYPE }, + {"free_text", ParamType::FREE_TEXT_PARAM_TYPE}, + {"html_input", ParamType::HTML_PARAM_TYPE}, + {"long_random_text", ParamType::LONG_RANDOM_TEXT_PARAM_TYPE}, + {"pipes", ParamType::PIPE_PARAM_TYPE}, + {"urls", ParamType::URL_PARAM_TYPE}, + {"local_file_path", ParamType::FILE_PATH_PARAM_TYPE}, + {"semicolon_delimiter", ParamType::SEMICOLON_DELIMITED_PARAM_TYPE}, + {"asterisk_delimiter", ParamType::ASTERISK_DELIMITED_PARAM_TYPE}, + {"comma_delimiter", ParamType::COMMA_DELIMITED_PARAM_TYPE}, + {"ampersand_delimiter", ParamType::AMPERSAND_DELIMITED_PARAM_TYPE}, + {"binary_input", ParamType::BINARY_PARAM_TYPE} + }; + + auto mapItr = sNameTypeMap.find(typeStr); + if (mapItr != sNameTypeMap.end()) + { + return mapItr->second; + } + dbgWarning(D_WAAP) << "unrecognized parameter type name: " << typeStr; + return UNKNOWN_PARAM_TYPE; + +} + + +} +} diff --git a/components/security_apps/waap/waap_clib/Waf2Util.h b/components/security_apps/waap/waap_clib/Waf2Util.h new file mode 100755 index 0000000..6b47d22 --- /dev/null +++ b/components/security_apps/waap/waap_clib/Waf2Util.h @@ -0,0 +1,1154 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __WAF2_UTIL_H__148aa7e4 +#define __WAF2_UTIL_H__148aa7e4 + +#include "WaapValueStatsAnalyzer.h" +#include "log_generator.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "WaapEnums.h" +#include "yajl/yajl_gen.h" + +#define GCC_VERSION (__GNUC__ * 10000 + __GNUC_MINOR__ * 100 + __GNUC_PATCHLEVEL__) + +// This is portable version of stricmp(), which is non-standard function (not even in C). +// Contrary to stricmp(), for a slight optimization, s2 is ASSUMED to be already in lowercase. +// s1 can be in mixed case and is convetred using tolower() before comparing to s2. +// The function returns true if s1 (with all charactes lowered case) matches s2, false if not. +inline bool my_stricmp(const char *s1, const char *s2) { + assert(s1 != NULL); + assert(s2 != NULL); + + // Compare header name, case insensitively, to "content-type" + while (*s1 && *s2 && tolower(*s1)==*s2) { + s1++; + s2++; + } + + // returns true if s1 (after applying tolower()) eactly matches s2 + return (*s1=='\0' && *s2=='\0'); +} + +// same as my_stricmp(), but assumes s1 has known size, and does not assume s1 string is null-terminated. +inline bool my_strincmp(const char *s1, const char *s2, size_t s1_size) { + assert(s1 != NULL); + assert(s2 != NULL); + + // Compare header name, case insensitively, to "content-type" + while (s1_size > 0 && *s2 && tolower(*s1)==*s2) { + s1++; + s2++; + s1_size--; // reduce s1_size until we exhaust at most s1_size characters of the s1 string. + } + + // returns true if s1 (after applying tolower()) eactly matches s2 + return (s1_size==0 && *s2=='\0'); +} + +inline bool my_stristarts_with(const char *s1, const char *s2) { + assert(s1 != NULL); + assert(s2 != NULL); + + // Compare case insensitively + while (*s1 && *s2 && tolower(*s1)==*s2) { + s1++; + s2++; + } + + // returns true if s1 (after applying tolower()) starts with s2 + // (this happens when we finished to examine all s2 and it compared correctly to start of s1) + return (*s2=='\0'); +} + +inline unsigned char from_hex(unsigned char ch, bool &valid) { + valid = true; + + if (ch <= '9' && ch >= '0') + ch -= '0'; + else if (ch <= 'f' && ch >= 'a') + ch -= 'a' - 10; + else if (ch <= 'F' && ch >= 'A') + ch -= 'A' - 10; + else { + valid = false; + ch = 0; + } + + return ch; +} + +template +_IT unquote_plus(_IT first, _IT last, bool decodeUrl=true, bool decodePlus=true) { + _IT result = first; + enum { STATE_COPY, STATE_FIRST_DIGIT, STATE_SECOND_DIGIT } state = STATE_COPY; + unsigned char accVal = 0; // accumulated character (from hex digits) + char lastCh = 0; + + for (; first != last; ++first) { + switch (state) { + case STATE_COPY: + if (*first == '+' && decodePlus) { + *result++ = ' '; + } + else if (decodeUrl && *first == '%') { + state = STATE_FIRST_DIGIT; + } + else { + *result++ = *first; + } + + break; + case STATE_FIRST_DIGIT: { + bool valid; + lastCh = *first; // remember it solely for special case where 2nd character is invalid hex + accVal = from_hex(*first, valid); + + if (valid) { + state = STATE_SECOND_DIGIT; + } + else { + *result++ = '%'; // put the percent symbol to the output stream + if (*first == '%') { + // we found the '%%' sequence. Put back the first '%' character and continue + // in the same state (as if we've just seen the first '%') + // this supports the case of %%xx, which would otherwise fail to parse. + } + else { + // put the "invalid" symbol to the output stream + *result++ = *first; + // continue copying + state = STATE_COPY; + } + } + + break; + } + case STATE_SECOND_DIGIT: { + bool valid; + accVal = (accVal << 4) | from_hex(*first, valid); + + if (valid) { + // After second hex digit decoded succesfully - put computed character to output and + // continue to "copying" state + *result++ = accVal; + } + else { + if (*first == '%') { + // put the percent symbol to the output + *result++ = '%'; + // put the first (among two) character (that was valid hex char), back to the output stream. + *result++ = lastCh; + state = STATE_FIRST_DIGIT; + break; + } + // If second character is invalid - return original '%', the first character, + // and the second character to the output. + + // put the percent symbol to the output + *result++ = '%'; + // put the first (among two) character (that was valid hex char), back to the output stream. + *result++ = lastCh; + // put the second (among two) "invalid" character to the output stream. + *result++ = *first; + } + + state = STATE_COPY; + break; + } + } + } + + if (state == STATE_FIRST_DIGIT) { + // put the percent symbol to the output stream + *result++ = '%'; + } + else if (state == STATE_SECOND_DIGIT) { + // put the percent symbol to the output + *result++ = '%'; + // put the first (among two) character (that was valid hex char), back to the output stream. + *result++ = lastCh; + } + + return result; +} + +inline bool isHexDigit(const char ch) { + return isdigit(ch) || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F'); +} + +template +_IT escape_backslashes(_IT first, _IT last) { + _IT result = first; + enum { STATE_COPY, STATE_ESCAPE, STATE_OCTAL, STATE_HEX } state = STATE_COPY; + unsigned char accVal = 0; + unsigned char digitsCount = 0; + _IT mark = first; + + for (; first != last; ++first) { + switch (state) { + case STATE_COPY: + if (*first == '\\') { + mark = first; + state = STATE_ESCAPE; + } + else { + *result++ = *first; + } + break; + case STATE_ESCAPE: { + if (*first >= '0' && *first <= '7') { + accVal = *first - '0'; + digitsCount = 1; + state = STATE_OCTAL; + break; + } else if (*first == 'x') { + accVal = 0; + digitsCount = 0; + state = STATE_HEX; + break; + } + else { + switch (*first) { + case 'a': *result++ = 7; break; // BELL + case 'b': *result++ = 8; break; // BACKSPACE + case 't': *result++ = 9; break; // HORIZONTAL TAB + case 'n': *result++ = 10; break; // LINEFEED + case 'v': *result++ = 11; break; // VERTICAL TAB + case 'f': *result++ = 12; break; // FORMFEED + case 'r': *result++ = 13; break; // CARRIAGE RETURN + case '\\': *result++ = '\\'; break; // upon seeing double backslash - output only one + case '\"': *result++ = '"'; break; // backslash followed by '"' - output only '"' + default: + // invalid escape sequence - do not replace it (return original characters) + // Copy from back-track, not including current character, and continue + while (mark < first) { + *result++ = *mark++; + } + + // Copy current (terminator) character which is not "escape" and return to copy state + // If current character is escape - stay is "escape" state + if (*first != '\\') { + *result++ = *mark++; + state = STATE_COPY; + } + } + + state = STATE_COPY; + } + + break; + } + case STATE_OCTAL: { + if (*first >='0' && *first<='7') { + accVal = (accVal << 3) | (*first - '0'); + digitsCount++; + + // Up to 3 octal digits imposed by C standard, so after 3 digits accumulation stops. + if (digitsCount == 3) { + *result++ = accVal; // output character corresponding to collected accumulated value + digitsCount = 0; + state = STATE_COPY; + } + } + else { + // invalid octal digit stops the accumulation + *result++ = accVal; // output character corresponding to collected accumulated value + digitsCount = 0; + if (*first != '\\') { + // If terminating character is not backslash output the terminating character + *result++ = *first; + state = STATE_COPY; + } + else { + // If terminating character is backslash start next escape sequence + state = STATE_ESCAPE; + } + } + + break; + } + case STATE_HEX: { + if (!isHexDigit(*first)) { + // Copy from back-track, not including current character (which is absent), and continue + while (mark < first) { + *result++ = *mark++; + } + if (*first != '\\') { + // If terminating character is not backslash output the terminating character + *result++ = *first; + state = STATE_COPY; + } + else { + // If terminating character is backslash start next escape sequence + state = STATE_ESCAPE; + } + } + else { + accVal = accVal << 4; + if (isdigit(*first)) { + accVal += *first - '0'; + } + else if (*first >= 'a' && *first <= 'f') { + accVal += *first - 'a' + 10; + } + else if (*first >= 'A' && *first <= 'F') { + accVal += *first - 'A' + 10; + } + digitsCount++; + // exactly 2 hex digits are anticipated, so after 2 digits accumulation stops. + if (digitsCount == 2) { + *result++ = accVal; // output character corresponding to collected accumulated value + digitsCount = 0; + state = STATE_COPY; + } + } + break; + } + } + } + + // Handle state at end of input + bool copyBackTrack = true; + switch (state) { + case STATE_HEX: + // this can only happen on this sequence '\xH' where H is a single hex digit. + // in this case the sequence is considered invalid and should be copied verbatim (copyBackTrack=true) + break; + case STATE_OCTAL: + // this can only happen when less than 3 octal digits are found at the value end, like '\1' or '\12' + *result++ = accVal; // output character corresponding to collected accumulated value + copyBackTrack = false; + break; + case STATE_COPY: + copyBackTrack = false; + break; + case STATE_ESCAPE: + break; + } + + if (copyBackTrack) { + // invalid escape sequence - do not replace it (return original characters) + // Copy from back-track + while (mark < first) { + *result++ = *mark++; + } + } + + return result; +} + +inline bool str_contains(const std::string &haystack, const std::string &needle) +{ + return haystack.find(needle) != std::string::npos; +} + +struct HtmlEntity { + const char *name; + unsigned short value; +}; + +extern const struct HtmlEntity g_htmlEntities[]; +extern const size_t g_htmlEntitiesCount; + +template +_IT escape_html(_IT first, _IT last) { + _IT result = first; + enum { + STATE_COPY, + STATE_ESCAPE, + STATE_NAMED_CHARACTER_REFERENCE, + STATE_NUMERIC_START, + STATE_NUMERIC, STATE_HEX + } state = STATE_COPY; + unsigned short accVal = 0; // should be unsigned short to hold unicode character code (16-bits) + bool digitsSeen = false; + std::list potentialMatchIndices; + size_t matchLength = 0; + size_t lastKnownMatchIndex = -1; + _IT mark = first; + + for (; first != last; ++first) { + switch (state) { + case STATE_COPY: + if (*first == '&') { + mark = first; + state = STATE_ESCAPE; + } + else { + *result++ = *first; + } + break; + case STATE_ESCAPE: + if (isalpha(*first)) { + // initialize potential matches list + potentialMatchIndices.clear(); + + for (size_t index = 0; index < g_htmlEntitiesCount; ++index) { + if (*first == g_htmlEntities[index].name[0]) { + potentialMatchIndices.push_back(index); + lastKnownMatchIndex = index; + } + } + + // No potential matches - send ampersand and current character to output + if (potentialMatchIndices.size() == 0) { + *result++ = '&'; + *result++ = *first; + state = STATE_COPY; + break; + } + + // 1st character already matched, so matchLen already starts from 1 + matchLength = 1; + state = STATE_NAMED_CHARACTER_REFERENCE; + } + else if (*first == '#') { + digitsSeen = 0; + accVal = 0; + state = STATE_NUMERIC_START; + } + else { + // not isalpha and not '#' - this is invalid character reference - do not replace it + // (return original characters) + *result++ = '&'; + *result++ = *first; + state = STATE_COPY; + } + break; + + case STATE_NAMED_CHARACTER_REFERENCE: + // Find and remove all potential matches that do not match anymore + { + int increaseMatchLength = 0; + + for ( + std::list::iterator pPotentialMatchIndex = potentialMatchIndices.begin(); + pPotentialMatchIndex != potentialMatchIndices.end(); + ) { + lastKnownMatchIndex = *pPotentialMatchIndex; + const char *matchName = g_htmlEntities[lastKnownMatchIndex].name; + + // If there are no more characters in the potntial match name, + // or the next tested character doesn't match - kill the match + if ((matchName[matchLength] == '\0') || (matchName[matchLength] != *first)) { + // remove current element from the list of potential matches + pPotentialMatchIndex = potentialMatchIndices.erase(pPotentialMatchIndex); + } + else { + increaseMatchLength = 1; + ++pPotentialMatchIndex; + } + } + + matchLength += increaseMatchLength; + } + + // No more potential matches: unsuccesful match -> flush all consumed characters back to output stream + if (potentialMatchIndices.size() == 0) { + // Send consumed ampersand to the output + *result++ = '&'; + + // Send those matched characters (these are the same that we consumed) - to the output + for (size_t i = 0; i < matchLength; i++) { + *result++ = g_htmlEntities[lastKnownMatchIndex].name[i]; + } + + // Send the character that terminated our search for possible matches + *result++ = *first; + + // Continue copying text verbatim + state = STATE_COPY; + break; // note: this breaks out of the for() loop, not out of the switch + } + + // There are still potential matches and ';' is hit + if (*first == ';') { + // longest match found for the named character reference. + // translate it into output character(s) and we're done. + unsigned short value = g_htmlEntities[lastKnownMatchIndex].value; + + // Encode UTF code point as UTF-8 bytes + if (value < 0x80) { + *result++ = value; + } + else if (value < 0x800 ) { + *result++ = (value >> 6) | 0xC0; + *result++ = (value & 0x3F) | 0x80; + } + else { // (value <= 0xFFFF : always true because value type is unsigned short which is 16-bit + *result++ = (value >> 12) | 0xE0; + *result++ = ((value >> 6) & 0x3F) | 0x80; + *result++ = (value & 0x3F) | 0x80; + } + + // Continue copying text verbatim + state = STATE_COPY; + break; // note: this breaks out of the for() loop, not out of the switch + } + break; + case STATE_NUMERIC_START: + digitsSeen = false; + accVal = 0; + if (*first == 'x' || *first == 'X') { + state = STATE_HEX; + } + else if (isdigit(*first)) { + digitsSeen = true; + accVal = *first - '0'; + state = STATE_NUMERIC; + } + else { + // Sequence started with these two characters: '&#', and here is the third, non-digit character + + // Copy from back-track, not including current character, and continue + while (mark < first) { + *result++ = *mark++; + } + + if (*first == '&') { + // Terminator is also start of next escape sequence + mark = first; + state = STATE_ESCAPE; + break; + } + else { + // Copy the terminating character too + *result++ = *first; + } + state = STATE_COPY; + } + break; + case STATE_NUMERIC: + if (!isdigit(*first)) { + if (digitsSeen) { + // Encode UTF code point as UTF-8 bytes + if (accVal < 0x80) { + *result++ = accVal; + } + else if (accVal < 0x800 ) { + *result++ = (accVal >> 6) | 0xC0; + *result++ = (accVal & 0x3F) | 0x80; + } + else { // (accVal <= 0xFFFF : always true because accVal type is unsigned short which is 16-bit + *result++ = (accVal >> 12) | 0xE0; + *result++ = ((accVal >> 6) & 0x3F) | 0x80; + *result++ = (accVal & 0x3F) | 0x80; + } + } + else { + // Copy from back-track, not including current character (which is absent), and continue + while (mark < first) { + *result++ = *mark++; + } + } + + if (*first == '&') { + // Terminator is also start of next escape sequence + mark = first; + state = STATE_ESCAPE; + break; + } + else if (!digitsSeen || *first != ';') { + // Do not copy the ';' but do copy any other terminator + // Note: the ';' should remain in the output if there were no digits seen. + *result++ = *first; + } + state = STATE_COPY; + } + else { + digitsSeen = true; + accVal = accVal * 10 + *first - '0'; // TODO:: beware of integer overflow? + } + break; + case STATE_HEX: + if (!isHexDigit(*first)) { + if (digitsSeen) { + // Encode UTF code point as UTF-8 bytes + if (accVal < 0x80) { + *result++ = accVal; + } + else if (accVal < 0x800 ) { + *result++ = (accVal >> 6) | 0xC0; + *result++ = (accVal & 0x3F) | 0x80; + } + else { // (accVal <= 0xFFFF : always true because accVal type is unsigned short which is 16-bit + *result++ = (accVal >> 12) | 0xE0; + *result++ = ((accVal >> 6) & 0x3F) | 0x80; + *result++ = (accVal & 0x3F) | 0x80; + } + } + else { + // Copy from back-track, not including current character (which is absent), and continue + while (mark < first) { + *result++ = *mark++; + } + } + + if (*first == '&') { + // Terminator is also start of next escape sequence + mark = first; + state = STATE_ESCAPE; + break; + } + else if (!digitsSeen || *first != ';') { + // Do not copy the ';' but do copy any other terminator + // Note: the ';' should remain in the output if there were no digits seen. + *result++ = *first; + } + state = STATE_COPY; + } + else { + digitsSeen = true; + accVal = accVal << 4; + if (isdigit(*first)) { + accVal += *first - '0'; + } + else if (*first >= 'a' && *first <= 'f') { + accVal += *first - 'a' + 10; + } + else if (*first >= 'A' && *first <= 'F') { + accVal += *first - 'A' + 10; + } + } + break; + } + } + + if (state == STATE_ESCAPE) { + *result++ = '&'; + } + else if (state == STATE_NAMED_CHARACTER_REFERENCE && potentialMatchIndices.size() > 0) { + // Send consumed ampersand to the output + *result++ = '&'; + + // Send those matched characters (these are the same that we consumed) - to the output + for (size_t i = 0; i < matchLength; i++) { + // Even if there are multiple potential matches, all of them start with the same + // matchLength characters that we consumed! + *result++ = g_htmlEntities[lastKnownMatchIndex].name[i]; + } + } + if (state == STATE_HEX && !digitsSeen) { // Special case of "&#x" + // Copy from back-track, not including current character (which is absent), and continue + while (mark < first) { + *result++ = *mark++; + } + state = STATE_COPY; + } + else if (state == STATE_HEX || state == STATE_NUMERIC || state == STATE_NUMERIC_START) { + if (digitsSeen) { + // Encode UTF code point as UTF-8 bytes + if (accVal < 0x80) { + *result++ = accVal; + } + else if (accVal < 0x800 ) { + *result++ = (accVal >> 6) | 0xC0; + *result++ = (accVal & 0x3F) | 0x80; + } + else { // (accVal <= 0xFFFF : always true because accVal type is unsigned short which is 16-bit + *result++ = (accVal >> 12) | 0xE0; + *result++ = ((accVal >> 6) & 0x3F) | 0x80; + *result++ = (accVal & 0x3F) | 0x80; + } + } + else { + // Copy from back-track, not including current character (which is absent), and continue + while (mark < first) { + *result++ = *mark++; + } + state = STATE_COPY; + } + } + + return result; +} + +// Compare two buffers, case insensitive. Return true if they are equal (case-insensitive) +inline bool memcaseinsensitivecmp(const char *buf1, size_t buf1_len, const char *buf2, size_t buf2_len) { + if (buf1_len != buf2_len) { + return false; + } + + for (; buf1_len > 0; --buf1_len) { + if (tolower(*buf1++) != tolower(*buf2++)) { + return false; // different + } + } + + return true; // equal +} + +inline void replaceAll(std::string& str, const std::string& from, const std::string& to) { + if(from.empty()) { + return; + } + + size_t start_pos = 0; + + while((start_pos = str.find(from, start_pos)) != std::string::npos) { + str.replace(start_pos, from.length(), to); + start_pos += to.length(); // In case 'to' contains 'from', like replacing 'x' with 'yx' + } +} + +// Count items in v that are not in ignored_set +inline size_t countNotInSet(const std::vector &v, const std::set &ignored_set) { + size_t count = 0; + + for (const std::string &word : v) { + if (ignored_set.find(word) == ignored_set.end()) { + // not in ignored_set + count++; + } + } + + return count; +} + +// note: this algorithm may probably be rewritten with std::remove_if() and probably lambda, +// but this better done when we can finally use c++11 +inline void removeItemsMatchingSubstringOf(std::vector &v, const std::string& match) { + for (std::vector::iterator it=v.begin(); it != v.end();) { + // Remove items that are contained (substr) within the (longer or equal-length) match string. + if (match.find(*it) != std::string::npos) { + it = v.erase(it); + } + else { + ++it; + } + } +} + +// Detect whether unicode code is in the "Halfwidth and Fullwidth Forms" set convertable to ASCII. +inline bool isUnicodeHalfAndFullWidthRange(uint32_t code) { + return (code >= 0xFF01 && code <=0xFF5E); +} + +// Convert unicode code from the "Halfwidth and Fullwidth Forms" set to ASCII. +inline char convertFromUnicodeHalfAndFullWidthRange(uint32_t code) { + assert(isUnicodeHalfAndFullWidthRange(code)); + // Support set of unicode characters from the "Halfwidth and Fullwidth Forms" that are converted to ASCII + static const char *xlat = + "!\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~"; + return xlat[code - 0xFF01]; +} + +inline bool isSpecialUnicode(uint32_t code) { + return isUnicodeHalfAndFullWidthRange(code) + || 0x2028 == code || 0x2029 == code + || 0x2216 == code || 0xEFC8 == code || 0xF025 == code; +} + +inline char convertSpecialUnicode(uint32_t code) { + if (isUnicodeHalfAndFullWidthRange(code)) { + return convertFromUnicodeHalfAndFullWidthRange(code); + } + else if (0x2216 == code || 0xEFC8 == code || 0xF025 == code) + { + return '\\'; + } + // assuming 0x2028 == code || 0x2029 == code + else + { + return '\n'; + } +} + +inline void stripSpaces(std::string &text) { + std::string::iterator it = text.begin(); + std::string::iterator result = it; + + for (; it != text.end(); ++it) { + unsigned char ch = (unsigned char)(*it); + + // Include only non-space characters + if (!isspace(ch)) { + *result++ = ch; + } + } + + text.erase(result, text.end()); +} + +inline size_t countSubstrings(const std::string &str, const std::string &subStr) { + if (subStr.empty()) { + return str.size() + 1; // to conform to python's "str.count(subStr)" behavior when substr is empty string... + } + + size_t count = 0; + size_t pos = str.find(subStr); + + while( pos != std::string::npos) { + count++; + pos = str.find(subStr, pos + subStr.size()); + } + + return count; +} + + +// Test whether text starts one of the known HTML tag names +bool startsWithHtmlTagName(const char *text); + +// Normalizing URL means replacing any pure-numeric URL parts with the word "_num" +// The parameters part of the given uri is also stripped (the '?' character and anything after it). +std::string normalize_uri(const std::string &uri); + +std::string normalize_param(const std::string& param); + +// Analogous to python's text.decode('unicode_escape'), with the distinction that +// this function simply throws out the \uXXXX sequences instead of converting them to binary unicode sequences. +// This function performs in-place decoding, updating text string in progress. +void unescapeUnicode(std::string &text); + +// Try to find and decode UTF7 chunks +std::string filterUTF7(const std::string &text); + +bool +b64DecodeChunk( + const std::string &value, + std::string::const_iterator it, + std::string::const_iterator end, + std::string &decoded); + +std::vector +split(const std::string& s, char delim); + +namespace Waap { +namespace Util { + typedef bool (*RegexSubCallback_f)( + const std::string &value, + std::string::const_iterator b, + std::string::const_iterator e, + std::string &repl); + + void b64Decode( + const std::string &s, + RegexSubCallback_f cb, + int &decodedCount, + int &deletedCount, + std::string &outStr); + + // The original stdlib implementation of isalpha() supports locale settings which we do not really need. + // It is also proven to contribute to slow performance in some of the algorithms using it. + // This function has reduced functionality compared to stdlib isalpha(), but is much faster. + inline bool isAlphaAsciiFast(unsigned char ch) { + return ((unsigned int)ch | 32) - 'a' < 26; + } + + // Compare two objects referenced by pointer - comparison is done by value (comparing objects themselves) + // This is different from comparing object pointers. + template + bool compareObjects(_T &first, _T &second) + { + // If both are the same object (or both are equal to nullptr - then they are equivalent) + if (first == second) { + return true; + } + + // If pointers are different and at least one of them is nullptr, then the other is not nullptr - so they are + // not equivalent + if (first == nullptr || second == nullptr) { + return false; + } + + // At this point, both pointers are for sure not nullptr, so we can dereference and compare objects pointed by + return *first == *second; + } + + inline bool str_isalnum(const std::string & value) { + for (std::string::const_iterator pC = value.begin(); pC != value.end(); ++pC) { + if (!std::isalnum(*pC)) { + return false; // at least one non alphanumeric character detected + } + } + + return true; + } + + inline bool isAllDigits(const std::string & value) { + for (char ch : value) { + if (!isdigit(ch)) { + return false; // at least one non digit character detected + } + } + + return true; + } + + typedef std::map > map_of_stringlists_t; + + // Yajl generator (C++ RAII edition :) + struct Yajl { + yajl_gen g; + Yajl() :g(yajl_gen_alloc(NULL)) {} + ~Yajl() + { + yajl_gen_free(g); + } + + struct Map { + yajl_gen& g; + explicit Map(Yajl& y) : g(y.g) + { + yajl_gen_map_open(g); + } + ~Map() + { + yajl_gen_map_close(g); + } + void gen_null(const std::string& k) + { + yajl_gen_string(g, (unsigned char*)k.data(), k.size()); yajl_gen_null(g); + } + void gen_str(const std::string& k, const std::string& v) + { + yajl_gen_string(g, (unsigned char*)k.data(), k.size()); + yajl_gen_string(g, (unsigned char*)v.data(), v.size()); + } + void gen_bool(const std::string& k, bool v) + { + yajl_gen_string(g, (unsigned char*)k.data(), k.size()); yajl_gen_bool(g, v); + } + void gen_integer(const std::string& k, long long int v) + { + yajl_gen_string(g, (unsigned char*)k.data(), k.size()); yajl_gen_integer(g, v); + } + void gen_double(const std::string& k, double v) + { + yajl_gen_string(g, (unsigned char*)k.data(), k.size()); yajl_gen_double(g, v); + } + void gen_key(const std::string& k) + { + yajl_gen_string(g, (unsigned char*)k.data(), k.size()); + } + }; + + struct Array { + yajl_gen& g; + explicit Array(Yajl& y) :g(y.g) { yajl_gen_array_open(g); } + ~Array() { yajl_gen_array_close(g); } + void gen_null() { yajl_gen_null(g); } + void gen_str(const std::string& v) { yajl_gen_string(g, (unsigned char*)v.data(), v.size()); } + void gen_bool(bool v) { yajl_gen_bool(g, v); } + void gen_integer(long long int v) { yajl_gen_integer(g, v); } + void gen_double(double v) { yajl_gen_double(g, v); } + }; + + std::string get_json_str() const { + const unsigned char* buf; + size_t len; + yajl_gen_get_buf(g, &buf, &len); + return std::string((char*)buf, len); + } + }; + + enum ContentType { + CONTENT_TYPE_UNKNOWN, + CONTENT_TYPE_XML, + CONTENT_TYPE_JSON, + CONTENT_TYPE_HTML, + CONTENT_TYPE_MULTIPART_FORM, + CONTENT_TYPE_URLENCODED, + CONTENT_TYPE_WBXML, + CONTENT_TYPES_COUNT + }; + +// LCOV_EXCL_START Reason: coverage upgrade + inline const char* getContentTypeStr(enum ContentType contentType) { + static const char* contentTypeStr[] = { + "UNKNOWN", + "XML", + "JSON", + "HTML", + "MULTIPART_FORM", + "URLENCODED", + "WBXML" + }; + + if (contentType >= CONTENT_TYPES_COUNT) { + contentType = CONTENT_TYPE_UNKNOWN; + } + + return contentTypeStr[contentType]; + }; +// LCOV_EXCL_STOP + + static const std::string s_EncryptionKey = "KSO+hOFs1q5SkEnx8bvp67Om2zyHDD6ZJF4NHAa3R94=";; + static const std::string s_EncryptionIV = "sxJNyEO7i6YfA1p9CTglHw=="; + + // trim from start + static inline std::string <rim(std::string &s) { + s.erase(s.begin(), std::find_if(s.begin(), s.end(), + std::not1(std::ptr_fun(std::isspace)))); + return s; + } + + // trim from end + static inline std::string &rtrim(std::string &s) { + s.erase(std::find_if(s.rbegin(), s.rend(), + std::not1(std::ptr_fun(std::isspace))).base(), s.end()); + return s; + } + + // trim from both ends + static inline std::string &trim(std::string &s) { + return ltrim(rtrim(s)); + } + + // Find whether some word (what) exists wihin keys of the map. + // The search done by *searching* for "what" string within each key string, + // not by *comparing* "what" with each key string. + bool find_in_map_of_stringlists_keys(const std::string & what, const map_of_stringlists_t & where); + + void remove_in_map_of_stringlists_keys(const std::string & what, map_of_stringlists_t & where); + + void remove_startswith(std::vector &vec, const std::string &prefix); + + std::string AES128Decrypt(std::string& key, std::string& iv, std::string& message); + std::string base64Encode(const std::string &input); + std::string base64Decode(const std::string &input); + std::string obfuscateXor(const std::string& toEncrypt); + std::string obfuscateXorBase64(const std::string& toEncrypt); + + bool containsInvalidUtf8(const std::string &payload); + + // based on invalid utf-8 evasion from here: https://www.cgisecurity.com/lib/URLEmbeddedAttacks.html + std::string unescapeInvalidUtf8(const std::string &text); + + bool containsBrokenUtf8(const std::string &payload); + std::string unescapeBrokenUtf8(const std::string &text); + + bool testUrlBareUtf8Evasion(const std::string &line); + bool testUrlBadUtf8Evasion(const std::string &line); + + std::string urlDecode(std::string src); + + std::string injectSpacesToString(const std::string& std); + + std::string charToString(const char* s, int slen); + + std::string vecToString(const std::vector& vec, char delim = ','); + template + std::string + setToString(const std::set& set, bool addParenthesis=true) { + std::ostringstream vts; + + if (addParenthesis) + { + vts << "["; + } + + if (!set.empty()) + { + for (auto itr = set.begin(); itr != set.end(); itr++) + { + vts << *itr << ", "; + } + } + else + { + return std::string(); + } + std::string res = vts.str(); + res.pop_back(); + res.pop_back(); + if (addParenthesis) + { + res += "]"; + } + + + return res; + } + + template + void mergeFromVectorWithoutDuplicates( + const std::vector& first_vector, + std::vector& second_vector) + { + for (const V& element : first_vector) + { + if(find(second_vector.begin(), second_vector.end(), element) == second_vector.end()) + { + second_vector.push_back(element); + } + } + } + + template + void mergeFromMapOfVectorsWithoutDuplicates( + const std::map>& first_map, + std::map>& second_map) + { + for (auto itr = first_map.begin(); itr != first_map.end(); itr++) + { + if (second_map.find(itr->first) != second_map.end()) + { + const std::vector& first_vector = first_map.at(itr->first); + mergeFromVectorWithoutDuplicates(first_vector, second_map[itr->first]); + } + else + { + const std::vector& first_vector = itr->second; + second_map[itr->first] = first_vector; + } + } + } + + template + void mergeSets(const std::set& first_set, const std::set& second_set, std::set& merged_set) + { + std::set_union( + first_set.begin(), + first_set.end(), + second_set.begin(), + second_set.end(), + std::inserter(merged_set, merged_set.begin()) + ); + } + + + ReportIS::Severity computeSeverityFromThreatLevel(ThreatLevel threatLevel); + ReportIS::Priority computePriorityFromThreatLevel(ThreatLevel threatLevel); + std::string computeConfidenceFromThreatLevel(ThreatLevel threatLevel); + + void decodePercentEncoding(std::string &text, bool decodePlus=false); + void decodeUtf16Value(const ValueStatsAnalyzer &valueStats, std::string &cur_val); + + std::string stripOptionalPort(const std::string::const_iterator &first, const std::string::const_iterator &last); + std::string extractKeyValueFromCookie(const std::string &cookie, const std::string &key); + bool isIpAddress(const std::string &ip_address); + bool vectorStringContain(const std::vector& vec, const std::string& str); + bool isIpTrusted(const std::string &ip, const std::vector &trusted_ips); + + + ContentType detectContentType(const char* hdr_value); + std::string convertParamTypeToStr(ParamType type); + ParamType convertTypeStrToEnum(const std::string& typeStr); + +} +} + +#endif // __WAF2_UTIL_H__148aa7e4 diff --git a/components/security_apps/waap/waap_clib/lru_cache_map.h b/components/security_apps/waap/waap_clib/lru_cache_map.h new file mode 100755 index 0000000..6a3a139 --- /dev/null +++ b/components/security_apps/waap/waap_clib/lru_cache_map.h @@ -0,0 +1,113 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include + +template +class LruCacheMap { +public: + // Type that should be passed to the insert() method + typedef std::pair value_type; +private: + struct TagQueue {}; + struct TagHash {}; + + // Multi-Index container implementing both queue and hashmap + typedef boost::multi_index::multi_index_container< + value_type, + boost::multi_index::indexed_by< + // Interface #0 (default) - sequenced (std::list) + boost::multi_index::sequenced< + boost::multi_index::tag + >, + // Interface #1 - hashed (std::unordered_map) + boost::multi_index::hashed_unique< + boost::multi_index::tag, + boost::multi_index::member< + value_type, + KeyType, + &value_type::first // hash by the key + > + > + > + > container_type; + + typedef typename container_type::template index::type container_queue_index_type; + typedef typename container_type::template index::type container_hash_index_type; +public: + // Allow iteration + typedef typename container_type::template index::type::iterator iterator; + typedef typename container_type::template index::type::const_iterator const_iterator; + iterator begin() { return m_queueIndex.begin(); } + iterator end() { return m_queueIndex.end(); } + const_iterator cbegin() const { return m_queueIndex.cbegin(); } + const_iterator cend() const { return m_queueIndex.cend(); } + + // Container constructor + LruCacheMap(int capacity) + :m_capacity(capacity), + m_queueIndex(m_container.template get()), + m_hashIndex(m_container.template get()) + {} + + // Get capacity + std::size_t capacity() const { return m_capacity; } + // Get count of entries stored + std::size_t size() const { return m_queueIndex.size(); } + // Return true if cache is empty + bool empty() const { return m_queueIndex.empty(); } + // Clear the cache + void clear() { m_queueIndex.clear(); } + + // Check if key exists by quickly looking in a hashmap + bool exist(const KeyType &key) const { + return m_hashIndex.find(key) != m_hashIndex.end(); + } + + bool get(const KeyType &key, ValueType &value) const { + // get the std::unordered_map index + const auto &found = m_hashIndex.find(key); + if (found == m_hashIndex.end()) { + // Value not found. Do not touch the value and return false. + return false; + } + // Value found - fill out the value and return true + value = found->second; + return true; + } + + // Insert entry into an LRU cache + void insert(const value_type &item) { + // Try to push a new entry to the front (may be rejected due to the hashed_unique index) + std::pair p = m_queueIndex.push_front(item); + if (!p.second) { + // not inserted - entry already existed - relocate the entry to the queue front + m_queueIndex.relocate(m_queueIndex.begin(), p.first); + } + else if (m_queueIndex.size() > m_capacity) { + // remove old unused entries at queue back to keep entries count under capacity + m_queueIndex.pop_back(); + } + } + +private: + std::size_t m_capacity; + container_type m_container; + container_queue_index_type &m_queueIndex; + container_hash_index_type &m_hashIndex; +}; diff --git a/components/security_apps/waap/waap_clib/lru_cache_set.h b/components/security_apps/waap/waap_clib/lru_cache_set.h new file mode 100755 index 0000000..04c0234 --- /dev/null +++ b/components/security_apps/waap/waap_clib/lru_cache_set.h @@ -0,0 +1,98 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include + +template +class LruCacheSet { +public: + // Type that should be passed to the insert() method + typedef KeyType value_type; +private: + struct TagQueue {}; + struct TagHash {}; + + // Multi-Index container implementing both queue and hashmap + typedef boost::multi_index::multi_index_container< + value_type, + boost::multi_index::indexed_by< + // Interface #0 (default) - sequenced (std::list) + boost::multi_index::sequenced< + boost::multi_index::tag + >, + // Interface #1 - hashed (std::unordered_set) + boost::multi_index::hashed_unique< + boost::multi_index::tag, + boost::multi_index::identity + > + > + > container_type; + + typedef typename container_type::template index::type container_queue_index_type; + typedef typename container_type::template index::type container_hash_index_type; +public: + // Allow iteration + typedef typename container_type::template index::type::iterator iterator; + typedef typename container_type::template index::type::const_iterator const_iterator; + iterator begin() { return m_queueIndex.begin(); } + iterator end() { return m_queueIndex.end(); } + const_iterator cbegin() const { return m_queueIndex.cbegin(); } + const_iterator cend() const { return m_queueIndex.cend(); } + + // Container constructor + LruCacheSet(int capacity) + :m_capacity(capacity), + m_queueIndex(m_container.template get()), + m_hashIndex(m_container.template get()) + {} + + // Get capacity + std::size_t capacity() const { return m_capacity; } + // Get count of entries stored + std::size_t size() const { return m_queueIndex.size(); } + // Return true if cache is empty + bool empty() const { return m_queueIndex.empty(); } + // Clear the cache + void clear() { m_queueIndex.clear(); } + + // Check if key exists by quickly looking in a hashmap + bool exist(const KeyType &key) const { + //m_queueIndex.hash_function(); <-- TODO:: remove -- test that this is indeed hash interface! + return m_hashIndex.find(key) != m_hashIndex.end(); + } + + // Insert entry into an LRU cache + void insert(const value_type &item) { + // Try to push a new entry to the front (may be rejected due to the hashed_unique index) + std::pair p = m_queueIndex.push_front(item); + if (!p.second) { + // not inserted - entry already existed - relocate the entry to the queue front + m_queueIndex.relocate(m_queueIndex.begin(), p.first); + } + else if (m_queueIndex.size() > m_capacity) { + // remove old unused entries at queue back to keep entries count under capacity + m_queueIndex.pop_back(); + } + } + +private: + std::size_t m_capacity; + container_type m_container; + container_queue_index_type &m_queueIndex; + container_hash_index_type &m_hashIndex; +}; diff --git a/components/security_apps/waap/waap_clib/waf2_reporting.h b/components/security_apps/waap/waap_clib/waf2_reporting.h new file mode 100755 index 0000000..1a77d6b --- /dev/null +++ b/components/security_apps/waap/waap_clib/waf2_reporting.h @@ -0,0 +1,198 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __WAF2_REPORTING__001de2f8 +#define __WAF2_REPORTING__001de2f8 + +// Generates data in JSON structure similar to what mod_security generates for its audit log +#include +#include +#include "yajl/yajl_gen.h" +#include "yajl/yajl_version.h" + +#define yajl_string(s) yajl_gen_string(g, (const unsigned char *)(s), strlen(s)) +#define yajl_string_len(s, l) yajl_gen_string(g, (const unsigned char *)(s), l) + +#define yajl_kv_null(k) yajl_string(k); yajl_gen_null(g) +#define yajl_kv_int(k, v) yajl_string(k); yajl_gen_integer(g, v) +#define yajl_kv_bool(k, v) yajl_string(k); yajl_gen_bool(g, v) +#define yajl_kv_string(k, v) yajl_string(k); yajl_string(v) +#define yajl_kv_string_len(k, v, vlen) yajl_string(k); yajl_string_len(v, vlen) + +typedef yajl_gen reporting_ctx_t; + +inline reporting_ctx_t +reporting_ctx_create() +{ + return yajl_gen_alloc(NULL); +} + +inline static void +reporting_ctx_free(reporting_ctx_t g) +{ + yajl_gen_free(g); +} + +inline void +reporting_start_report(reporting_ctx_t g) +{ + yajl_gen_map_open(g); +} + +inline void +reporting_emit_transaction_info( + reporting_ctx_t g, const char *log_time, + const char *transaction_id, + const char *remote_addr, + int remote_port, + const char *local_addr, + int local_port) +{ + yajl_string("transaction"); + yajl_gen_map_open(g); + yajl_kv_string("time", log_time); + yajl_kv_string("transaction_id", transaction_id); + yajl_kv_string("remote_address", remote_addr); + yajl_kv_int("remote_port", remote_port); + yajl_kv_string("local_address", local_addr); + yajl_kv_int("local_port", local_port); + yajl_gen_map_close(g); +} + +// Request +inline void +reporting_start_request(reporting_ctx_t g, const char *uri) +{ + yajl_string("request"); + yajl_gen_map_open(g); + yajl_kv_string("uri", uri); +} + +inline void +reporting_start_request_hdrs(reporting_ctx_t g) +{ + yajl_string("headers"); + yajl_gen_map_open(g); +} + +inline void +reporting_add_request_hdr(reporting_ctx_t g, const char *name, int name_len, const char *value, int value_len) +{ + yajl_string_len(name, name_len); + yajl_string_len(value, value_len); +} + +inline void +reporting_end_request_hdrs(reporting_ctx_t g) +{ + yajl_gen_map_close(g); +} + +inline void +reporting_start_request_body(reporting_ctx_t g) +{ + yajl_string("body"); + yajl_gen_array_open(g); +} + +inline void +reporting_add_request_body_chunk(reporting_ctx_t g, const char *data, int data_len) +{ + yajl_string_len(data, data_len); +} + +inline void +reporting_end_request_body(reporting_ctx_t g) +{ + yajl_gen_array_close(g); +} + +inline void +reporting_end_request(reporting_ctx_t g) +{ + yajl_gen_map_close(g); +} + +// Response +inline void +reporting_start_response(reporting_ctx_t g, int response_status, int http_version) +{ + yajl_string("response"); + yajl_gen_map_open(g); + yajl_kv_string("protocol", (http_version==1) ? "HTTP/1.1" : "HTTP/1.0"); + // as an integer, response status is easier to parse than status_line + yajl_kv_int("status", response_status); +} + +inline void +reporting_start_response_hdrs(reporting_ctx_t g) +{ + yajl_string("headers"); + yajl_gen_map_open(g); +} + +inline void +reporting_add_response_hdr(reporting_ctx_t g, const char *name, int name_len, const char *value, int value_len) +{ + yajl_string_len(name, name_len); + yajl_string_len(value, value_len); +} + +inline void +reporting_end_response_hdrs(reporting_ctx_t g) +{ + yajl_gen_map_close(g); +} + +inline void +reporting_start_response_body(reporting_ctx_t g) +{ + yajl_string("body"); + yajl_gen_array_open(g); +} + +inline void +reporting_add_response_body_chunk(reporting_ctx_t g, const char *data, int data_len) +{ + yajl_string_len(data, data_len); +} + +inline void +reporting_end_response_body(reporting_ctx_t g) +{ + yajl_gen_array_close(g); +} + +inline void +reporting_end_response(reporting_ctx_t g) +{ + yajl_gen_map_close(g); +} + +inline void +reporting_end_report(reporting_ctx_t g) +{ + yajl_gen_map_close(g); +} + +inline void +reporting_dump_report(reporting_ctx_t g, FILE *f) +{ + const unsigned char *final_buf; + size_t len; + yajl_gen_get_buf(g, &final_buf, &len); + fwrite(final_buf, 1, len, f); + yajl_gen_clear(g); +} + +#endif // __WAF2_REPORTING__001de2f8 diff --git a/components/security_apps/waap/waap_component.cc b/components/security_apps/waap/waap_component.cc new file mode 100755 index 0000000..8a4ad8c --- /dev/null +++ b/components/security_apps/waap/waap_component.cc @@ -0,0 +1,73 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include +#include "waap.h" +#include "telemetry.h" +#include "waap_clib/DeepAnalyzer.h" +#include "waap_component_impl.h" +#include "debug.h" +#include "waap_clib/WaapConfigApplication.h" +#include "waap_clib/WaapConfigApi.h" + +USE_DEBUG_FLAG(D_WAAP); +USE_DEBUG_FLAG(D_WAAP_API); + +WaapComponent::WaapComponent() : Component("WaapComponent"), pimpl(std::make_unique()) +{ + dbgTrace(D_WAAP) << "WaapComponent::WaapComponent()"; +} + +WaapComponent::~WaapComponent() +{ + dbgTrace(D_WAAP) << "WaapComponent::~WaapComponent()"; +} + +void +WaapComponent::init() +{ + pimpl->init(); +} + +void +WaapComponent::fini() +{ + pimpl->fini(); +} + +void +WaapComponent::preload() +{ + // TODO:: call stuff like registerExpectedCofiguration here.. + registerExpectedConfiguration("WAAP", "WebApplicationSecurity"); + registerExpectedConfiguration("WAAP", "WebAPISecurity"); + registerExpectedConfiguration("WAAP", "Sigs score file path"); + registerExpectedConfiguration("WAAP", "Sigs file path"); + registerExpectedConfigFile("waap", Config::ConfigFileType::Policy); + registerConfigLoadCb( + [this]() + { + WaapConfigApplication::notifyAssetsCount(); + WaapConfigAPI::notifyAssetsCount(); + } + ); + registerConfigPrepareCb( + [this]() + { + WaapConfigApplication::clearAssetsCount(); + WaapConfigAPI::clearAssetsCount(); + } + ); + dbgTrace(D_WAAP) << "WaapComponent::preload() exit"; +} diff --git a/components/security_apps/waap/waap_component_impl.cc b/components/security_apps/waap/waap_component_impl.cc new file mode 100755 index 0000000..bf13ffb --- /dev/null +++ b/components/security_apps/waap/waap_component_impl.cc @@ -0,0 +1,750 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#define WAF2_LOGGING_ENABLE + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "debug.h" +#include "waap_clib/WaapAssetStatesManager.h" +#include "waap_clib/Waf2Engine.h" +#include "waap_clib/WaapConfigApi.h" +#include "waap_clib/WaapConfigApplication.h" +#include "waap_clib/WaapDecision.h" +#include "telemetry.h" +#include "waap_clib/DeepAnalyzer.h" +#include "waap_component_impl.h" +#include "i_waapConfig.h" +#include "generic_rulebase/rulebase_config.h" +#include "report_messaging.h" +#include "first_request_object.h" + +using namespace std; + +USE_DEBUG_FLAG(D_WAAP); +USE_DEBUG_FLAG(D_WAAP_ULIMITS); + +WaapComponent::Impl::Impl() : + pending_response(ngx_http_cp_verdict_e::TRAFFIC_VERDICT_INSPECT), + accept_response(ngx_http_cp_verdict_e::TRAFFIC_VERDICT_ACCEPT), + drop_response(ngx_http_cp_verdict_e::TRAFFIC_VERDICT_DROP), + waapStateTable(NULL), + transactionsCount(0), + deepAnalyzer() +{ +} + +WaapComponent::Impl::~Impl() +{ +} + +// Called when component is initialized +void +WaapComponent::Impl::init() +{ + std::string sigs_file_path = getConfigurationWithDefault( + "/etc/cp/conf/waap/1.data", + "WAAP", + "Sigs file path" + ); + + std::string sigs_score_file_path = getConfigurationWithDefault( + "/etc/cp/conf/waap/2.data", + "WAAP", + "Sigs score file path" + ); + + assets_metric.init( + "Assets Count", + ReportIS::AudienceTeam::AGENT_CORE, + ReportIS::IssuingEngine::AGENT_CORE, + std::chrono::minutes(10), + true, + ReportIS::Audience::INTERNAL + ); + assets_metric.registerListener(); + registerListener(); + waap_metric.registerListener(); + + init(sigs_file_path, sigs_score_file_path); +} + +void +WaapComponent::Impl::init(const std::string &sigs_file_path, const std::string &sigs_scores_file_path) +{ + //waf2_set_log_target(WAF2_LOGTARGET_STDERR); + dbgTrace(D_WAAP) << "WaapComponent::Impl::init() ..."; + + reputationAggregator.init(); + + bool success = waf2_proc_start(sigs_file_path, sigs_scores_file_path); + if (!success) { + dbgWarning(D_WAAP) << "WAF2 engine FAILED to initialize (probably failed to load signatures). Aborting!"; + waf2_proc_exit(); + return; + } + + dbgTrace(D_WAAP) << "WaapComponent::Impl::init() signatures loaded succesfully."; + + I_StaticResourcesHandler *static_resources = Singleton::Consume::by(); + static_resources->registerStaticResource("cp-ab.js", "/etc/cp/conf/waap/cp-ab.js"); + static_resources->registerStaticResource("cp-csrf.js", "/etc/cp/conf/waap/cp-csrf.js"); + + waapStateTable = Singleton::Consume::by(); +} + +// Called when component is shut down +void +WaapComponent::Impl::fini() +{ + dbgTrace(D_WAAP) << "WaapComponent::impl::fini(). Shutting down waap engine before exiting..."; + unregisterListener(); + waf2_proc_exit(); +} + +std::string +WaapComponent::Impl::getListenerName() const +{ + return "waap application"; +} + +// Start request (called before headers arrive). However, the method and URL path is known at this stage. +// Should return pending_response to hold the data (not send to upstream) +EventVerdict +WaapComponent::Impl::respond(const NewHttpTransactionEvent &event) +{ + dbgTrace(D_WAAP) << " * \e[32mNGEN_EVENT: NewTransactionEvent\e[0m"; + + if (waapStateTable->hasState()) { + dbgWarning(D_WAAP) << " * \e[31 -- NewTransactionEvent called twice on same entry \e[0m"; + return drop_response; + } + + I_WaapAssetStatesManager* pWaapAssetStatesManager = + Singleton::Consume::by(); + std::shared_ptr pCurrentWaapAssetState = pWaapAssetStatesManager->getWaapAssetStateGlobal(); + + if (!pCurrentWaapAssetState || pCurrentWaapAssetState->getSignatures()->fail()) + { + dbgTrace(D_WAAP) << "WaapComponent::Impl::UponEvent(NewTransactionEvent): couldn't get WaapAssetState ..."; + return drop_response; + } + + dbgTrace(D_WAAP) << "WaapComponent::Impl::UponEvent(NewTransactionEvent): creating state..."; + if(!waapStateTable->createState(pCurrentWaapAssetState)) { + dbgWarning(D_WAAP) << " * \e[31 -- NewTransactionEvent failed to create new state in table\e[0m"; + return drop_response; + } + + if (!waapStateTable->hasState()) { + dbgWarning(D_WAAP) << " * \e[31 -- NewTransactionEvent state was created but still missing \e[0m"; + return drop_response; + } + + IWaf2Transaction& waf2Transaction = waapStateTable->getState(); + + // Assign unique numeric index to this transaction + waf2Transaction.setIndex(transactionsCount++); + + std::string uri = event.getURI(); + std::string httpMethodStr = event.getHttpMethod(); + + dbgTrace(D_WAAP) << "start Transaction: " << httpMethodStr << " " << uri << " (REQUEST)"; + + // See below.. + Waf2TransactionFlags &waf2TransactionFlags = waf2Transaction.getTransactionFlags(); + waf2TransactionFlags.requestDataPushStarted = false; + waf2TransactionFlags.endResponseHeadersCalled = false; + waf2TransactionFlags.responseDataPushStarted = false; + + waf2Transaction.start(); + + char sourceIpStr[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &(event.getSourceIP()), sourceIpStr, INET_ADDRSTRLEN); + + char listeningIpStr[INET_ADDRSTRLEN]; + inet_ntop(AF_INET, &(event.getListeningIP()), listeningIpStr, INET_ADDRSTRLEN); + + // Set envelope data + waf2Transaction.set_transaction_remote(sourceIpStr, event.getSourcePort()); + waf2Transaction.set_transaction_local(listeningIpStr, event.getListeningPort()); + + waf2Transaction.set_method(httpMethodStr.c_str()); + waf2Transaction.set_uri(uri.c_str()); + + // Tell waf2 API that request headers started + waf2Transaction.start_request_hdrs(); + + return pending_response; +} + +// Request headers coming +// Should return pending_response to hold the data (not send to upstream) +EventVerdict +WaapComponent::Impl::respond(const HttpRequestHeaderEvent &event) +{ + auto &header_name = event.getKey(); + auto &header_value = event.getValue(); + + dbgTrace(D_WAAP) + << " * \e[32mNGEN_EVENT: HttpHeaderRequest event: " + << string(header_name) + << ": " + << string(header_value) + << "\e[0m"; + + if (!waapStateTable->hasState()) { + dbgWarning(D_WAAP) + << " * \e[31mNGEN_EVENT: http_header - " + << "failed to get waf2 transaction, state not exist\e[0m"; + return drop_response; + } + IWaf2Transaction& waf2Transaction = waapStateTable->getState(); + + // Tell waf2 API that another request header arrived + waf2Transaction.add_request_hdr( + reinterpret_cast(header_name.data()), //const char * name // + header_name.size(), //int name_len // + reinterpret_cast(header_value.data()), //const char * value // + header_value.size() //int value_len // + ); + + EventVerdict verdict = pending_response; + + // Last header handled + if (event.isLastHeader()) { + waf2Transaction.end_request_hdrs(); + + verdict = waf2Transaction.getUserLimitVerdict(); + + if (verdict.getVerdict() == pending_response.getVerdict()) { + // waapDecision returns one of these verdicts: accept, drop, pending + // PENDING verdict (also called INSPECT by ngen core) will be returned if the waap engine wants to also + // inspect response. + verdict = waapDecisionAfterHeaders(waf2Transaction); + } + + } + + // Delete state before returning any verdict which is not pending + if ((verdict.getVerdict() != pending_response.getVerdict()) && waapStateTable->hasState()) { + finishTransaction(waf2Transaction); + } + + return verdict; +} + +// Request body pieces coming. +// Should return pending_response to hold the data (not send to upstream) +EventVerdict +WaapComponent::Impl::respond(const HttpRequestBodyEvent &event) +{ + dbgTrace(D_WAAP) << " * \e[32mNGEN_EVENT: HttpBodyRequest data buffer event\e[0m"; + + if (!waapStateTable->hasState()) { + dbgWarning(D_WAAP) << + " * \e[31mNGEN_EVENT: data buffer - failed to get waf2 transaction, state not exist\e[0m"; + return drop_response; + } + + IWaf2Transaction& waf2Transaction = waapStateTable->getState(); + Waf2TransactionFlags &waf2TransactionFlags = waf2Transaction.getTransactionFlags(); + + // Do this only once (on first request body data packet) + if (!waf2TransactionFlags.requestDataPushStarted) { + dbgTrace(D_WAAP) << "first request body packet"; + waf2Transaction.start_request_body(); + waf2TransactionFlags.requestDataPushStarted = true; + } + + // Push the request data chunk to the waf2 engine + const char *dataBuf = (const char*)event.getData().data(); + size_t dataBufLen = event.getData().size(); + + waf2Transaction.add_request_body_chunk(dataBuf, dataBufLen); + + ngx_http_cp_verdict_e verdict = waf2Transaction.getUserLimitVerdict(); + if (verdict != ngx_http_cp_verdict_e::TRAFFIC_VERDICT_INSPECT) { + finishTransaction(waf2Transaction); + } + + return EventVerdict(verdict); +} + +// Called when request ends and response starts. +// For WAAP its time to decide and return either "accept_response" or "drop_response" +EventVerdict +WaapComponent::Impl::respond(const EndRequestEvent &) +{ + dbgTrace(D_WAAP) << " * \e[32mNGEN_EVENT: endRequest event\e[0m"; + + if (!waapStateTable->hasState()) { + dbgWarning(D_WAAP) + << "* \e[31mNGEN_EVENT: endRequest - failed to get waf2 transaction, state does not exist\e[0m"; + return drop_response; + } + + IWaf2Transaction& waf2Transaction = waapStateTable->getState(); + Waf2TransactionFlags &waf2TransactionFlags = waf2Transaction.getTransactionFlags(); + + // Do not forget to tell waf2 engine that data ended (if we started request_body above...) + if (waf2TransactionFlags.requestDataPushStarted) { + waf2Transaction.end_request_body(); + waf2TransactionFlags.requestDataPushStarted = false; + } + + // Tell waf2 engine that request stage is finished + waf2Transaction.end_request(); + + // waapDecision returns one of these verdicts: accept, drop, pending + // PENDING verdict (also called INSPECT by ngen core) will be returned if the waap engine wants to also inspect + // response. + EventVerdict verdict = waapDecision(waf2Transaction); + + // Delete state before returning any verdict which is not pending + if (verdict.getVerdict() != ngx_http_cp_verdict_e::TRAFFIC_VERDICT_INSPECT && + waapStateTable->hasState() + ) { + finishTransaction(waf2Transaction); + } + + return verdict; +} + +EventVerdict +WaapComponent::Impl::respond(const ResponseCodeEvent &event) +{ + dbgTrace(D_WAAP) + << " * \e[32mNGEN_EVENT: ResponseCodeTransactionEvent event code = " + << event.getResponseCode() + << "\e[0m"; + + if (!waapStateTable->hasState()) { + dbgWarning(D_WAAP) + << " * \e[31mNGEN_EVENT: ResponseCodeTransactionEvent - failed to get waf2 transaction, " + << "state does not exist\e[0m"; + return drop_response; + } + + IWaf2Transaction& waf2Transaction = waapStateTable->getState(); + + // TODO:: extract HTTP version from attachment? + static const int http_version = 0x11; + + // Tell waf2 API that response starts + waf2Transaction.start_response(event.getResponseCode(), http_version); + + EventVerdict verdict = pending_response; + + // Set drop verdict if waap engine decides to drop response. + if (!waf2Transaction.decideResponse()) { + dbgTrace(D_WAAP) << " * \e[32m ResponseCodeTransactionEvent: decideResponse: DROP\e[0m"; + verdict = drop_response; + } else if (!waf2Transaction.shouldInspectResponse()) { + // Set accept verdict if waap engine no more interested in response + dbgTrace(D_WAAP) << " * \e[32m ResponseCodeTransactionEvent: shouldInspectResponse==false: ACCEPT\e[0m"; + verdict = accept_response; + } else { + // Tell waf2 API that response headers start + waf2Transaction.start_response_hdrs(); + } + + // Delete state before returning any verdict which is not pending + if (verdict.getVerdict() != ngx_http_cp_verdict_e::TRAFFIC_VERDICT_INSPECT && + verdict.getVerdict() != ngx_http_cp_verdict_e::TRAFFIC_VERDICT_INJECT && + waapStateTable->hasState() + ) { + finishTransaction(waf2Transaction); + } + + return verdict; +} + +EventVerdict +WaapComponent::Impl::respond(const HttpResponseHeaderEvent &event) +{ + auto &header_name = event.getKey(); + auto &header_value = event.getValue(); + + dbgTrace(D_WAAP) + << " * \e[32mNGEN_EVENT: HttpHeaderResponse event: " + << string(header_name) + << ": " + << string(header_value) + << "\e[0m"; + + if (!waapStateTable->hasState()) { + dbgWarning(D_WAAP) + << " * \e[31mNGEN_EVENT: HttpHeaderResponse - " + << "failed to get waf2 transaction, state does not exist\e[0m"; + return drop_response; + } + + IWaf2Transaction& waf2Transaction = waapStateTable->getState(); + + // Send response header to the engine + waf2Transaction.add_response_hdr( + reinterpret_cast(header_name.data()), + header_name.size(), + reinterpret_cast(header_value.data()), + header_value.size() + ); + + ngx_http_cp_verdict_e verdict = pending_response.getVerdict(); + HttpHeaderModification modifications; + bool isSecurityHeadersInjected = false; + + if (waf2Transaction.shouldInjectSecurityHeaders()) { + dbgTrace(D_WAAP) << " * \e[32m HttpHeaderResponse: Trying to inject Security Headers\e[0m"; + if (event.isLastHeader()) { + dbgTrace(D_WAAP) << " * \e[32m HttpHeaderResponse: Injecting Security Headers\e[0m"; + std::vector> injectHeaderStr; + waf2Transaction.handleSecurityHeadersInjection(injectHeaderStr); + for (auto header : injectHeaderStr) { + dbgTrace(D_WAAP) << " * \e[32m HttpHeaderResponse: Injecting Security Header. Header name: \e[0m" << + header.first << " Header value: " << header.second; + Buffer headerValue(header.second); + HeaderKey headerName(header.first); + Maybe result = modifications.appendHeader(std::move(headerName), std::move(headerValue)); + if (!result.ok()) { + dbgWarning(D_WAAP) + << "Failed to inject (Security header) buffer in requested position. Buffer: " + << header.second + << ", position: " + << 0 + << ". Error: " + << result.getErr(); + } + } + isSecurityHeadersInjected = true; + verdict = ngx_http_cp_verdict_e::TRAFFIC_VERDICT_INJECT; + } + } + + if (waf2Transaction.shouldInjectCSRF()) { + if (event.isLastHeader()) { + std::string injectStr; + waf2Transaction.handleCsrfHeaderInjection(injectStr); + Buffer injected_buffer(injectStr); + HeaderKey setCookie("Set-Cookie"); + Maybe result = modifications.appendHeader(std::move(setCookie), std::move(injected_buffer)); + if (!result.ok()) { + dbgWarning(D_WAAP) + << "Failed to inject (CSRF header) buffer in requested position. Buffer: " + << injectStr + << ", position: " + << 0 + << ". Error: " + << result.getErr(); + } + verdict = ngx_http_cp_verdict_e::TRAFFIC_VERDICT_INJECT; + } + } + + // Set drop verdict if waap engine decides to drop response. + if (!waf2Transaction.decideResponse()) { + dbgTrace(D_WAAP) << " * \e[32m HttpHeaderResponse: decideResponse: DROP\e[0m"; + verdict = drop_response.getVerdict(); + } else if (!waf2Transaction.shouldInspectResponse()) { + // Set accept verdict if waap engine no more interested in response + dbgTrace(D_WAAP) << " * \e[32m HttpHeaderResponse: shouldInspectResponse==false: ACCEPT\e[0m"; + verdict = accept_response.getVerdict(); + } + + if (waf2Transaction.shouldInjectSecurityHeaders() && isSecurityHeadersInjected && + verdict == ngx_http_cp_verdict_e::TRAFFIC_VERDICT_INJECT + ) { + // disable should inject security headers after injection to avoid response body scanning when it's unnecessary + waf2Transaction.disableShouldInjectSecurityHeaders(); + } + + // Delete state before returning any verdict which is not pending + if (verdict != ngx_http_cp_verdict_e::TRAFFIC_VERDICT_INSPECT && + verdict != ngx_http_cp_verdict_e::TRAFFIC_VERDICT_INJECT && + waapStateTable->hasState() + ) { + finishTransaction(waf2Transaction); + } + + return EventVerdict(move(modifications.getModificationList()), verdict); +} + +EventVerdict +WaapComponent::Impl::respond(const HttpResponseBodyEvent &event) +{ + dbgTrace(D_WAAP) << " * \e[32mNGEN_EVENT: HttpBodyResponse data buffer event\e[0m"; + + if (!waapStateTable->hasState()) { + dbgWarning(D_WAAP) << + " * \e[31mNGEN_EVENT: HttpBodyResponse - failed to get waf2 transaction, state does not exist\e[0m"; + return drop_response; + } + + IWaf2Transaction& waf2Transaction = waapStateTable->getState(); + Waf2TransactionFlags &waf2TransactionFlags = waf2Transaction.getTransactionFlags(); + + // Do this only once (on first response body data packet) + if (!waf2TransactionFlags.responseDataPushStarted) { + dbgTrace(D_WAAP) << "first response body packet"; + + // Tell waf2 transaction that all response headers are finished + if (!waf2TransactionFlags.endResponseHeadersCalled) { + // At this point, all response headers were received + waf2Transaction.end_response_hdrs(); + waf2TransactionFlags.endResponseHeadersCalled = true; + } + + waf2Transaction.start_response_body(); + waf2TransactionFlags.responseDataPushStarted = true; + } + + dbgTrace(D_WAAP) << "HttpBodyResponse"; + + + // Push the response data chunk to the waf2 engine + const char *dataBuf = (const char*)event.getData().data(); + size_t dataBufLen = event.getData().size(); + + waf2Transaction.add_response_body_chunk(dataBuf, dataBufLen); + + ngx_http_cp_verdict_e verdict = pending_response.getVerdict(); + HttpBodyModification modifications; + + // Set drop verdict if waap engine decides to drop response. + if (!waf2Transaction.decideResponse()) { + dbgTrace(D_WAAP) << " * \e[32m HttpBodyResponse: decideResponse: DROP\e[0m"; + verdict = drop_response.getVerdict(); + } + + if (verdict == pending_response.getVerdict() && + waf2Transaction.shouldInjectResponse() && + !event.isLastChunk() + ) { + // Inject if needed. Note that this is only reasonable to do if there was no DROP decision above + + std::string injectionStr; + int pos = 0; + + if(waf2Transaction.isHtmlType(dataBuf, dataBufLen)) { + bool htmlTagFound = waf2Transaction.findHtmlTagToInject( + dataBuf, + dataBufLen, + pos + ); + + pos = htmlTagFound ? pos + 1 : 0; + + waf2Transaction.completeInjectionResponseBody(injectionStr); + dbgTrace(D_WAAP) << "HttpBodyResponse(): injectionStr: " << injectionStr << " pos: " << pos + << " URI: " << waf2Transaction.getUriStr(); + Maybe result = modifications.inject(pos, Buffer(injectionStr)); + if(!result.ok()) { + dbgWarning(D_WAAP) << "HttpBodyResponse(): Scripts injection failed!"; + } + verdict = ngx_http_cp_verdict_e::TRAFFIC_VERDICT_INJECT; + } else { + // This response body is not considered "HTML" - disable injection + dbgTrace(D_WAAP) << "HttpBodyResponse(): the response body is not HTML - disabling injection"; + + // Note that this operation might affect the shouldInspectResponse() state if injection was the only reason + // to inspect the response body. + waf2Transaction.clearAllInjectionReasons(); + } + } + + if (verdict == pending_response.getVerdict() && !waf2Transaction.shouldInspectResponse()) { + // Set accept verdict if waap engine no more interested in response + dbgTrace(D_WAAP) << " * \e[32m HttpBodyResponse: shouldInspectResponse==false: ACCEPT\e[0m"; + verdict = accept_response.getVerdict(); + } + + // Delete state before returning any verdict which is not pending or inject + if (verdict != ngx_http_cp_verdict_e::TRAFFIC_VERDICT_INSPECT && + verdict != ngx_http_cp_verdict_e::TRAFFIC_VERDICT_INJECT && + waapStateTable->hasState() + ) { + finishTransaction(waf2Transaction); + } + + return EventVerdict(modifications.getModificationList(), verdict); +} + +EventVerdict +WaapComponent::Impl::respond(const EndTransactionEvent &) +{ + if (!waapStateTable->hasState()) { + dbgWarning(D_WAAP) << + " * \e[31mNGEN_EVENT: endTransaction - failed to get waf2 transaction, state does not exist\e[0m"; + return EventVerdict(drop_response); + } + + IWaf2Transaction& waf2Transaction = waapStateTable->getState(); + Waf2TransactionFlags &waf2TransactionFlags = waf2Transaction.getTransactionFlags(); + + // Do not forget to tell waf2 engine that response headers ended. + if (!waf2TransactionFlags.endResponseHeadersCalled) { + waf2Transaction.end_response_hdrs(); + waf2TransactionFlags.endResponseHeadersCalled = true; + } else if (waf2TransactionFlags.responseDataPushStarted) { + // Do not forget to tell waf2 engine that data ended (if we started response_body above...) + waf2Transaction.end_response_body(); + waf2TransactionFlags.responseDataPushStarted = false; + } + + waf2Transaction.end_response(); + + EventVerdict verdict = accept_response; + + // Set drop verdict if waap engine decides to drop response. + if (!waf2Transaction.decideResponse()) { + dbgTrace(D_WAAP) << " * \e[32m endTransaction: decideResponse: DROP\e[0m"; + verdict = drop_response; + } else if (!waf2Transaction.shouldInspectResponse()) { + // Set accept verdict if waap engine no more interested in response + dbgTrace(D_WAAP) << " * \e[32m endTransaction: shouldInspectResponse==false: ACCEPT\e[0m"; + } + + // This is our last chance to delete the state. The verdict must not be "PENDING" at this point. + finishTransaction(waf2Transaction); + return verdict; +} + +EventVerdict +WaapComponent::Impl::waapDecisionAfterHeaders(IWaf2Transaction& waf2Transaction) +{ + dbgTrace(D_WAAP) << "waapDecisionAfterHeaders() started"; + if (waf2Transaction.decideAfterHeaders()) { + dbgTrace(D_WAAP) << "WaapComponent::Impl::waapDecisionAfterHeaders(): returning DROP response."; + return drop_response; + } + return pending_response; +} + +EventVerdict +WaapComponent::Impl::waapDecision(IWaf2Transaction& waf2Transaction) +{ + dbgTrace(D_WAAP) << "waapDecision() started"; + + static const int mode = 1; + AnalysisResult result; + int verdictCode = waf2Transaction.decideFinal(mode, result); + + EventVerdict verdict = accept_response; + + // Note: verdict is 0 if nothing suspicious, 1 if should block, or negative if error occurred + // (in the latter case - decision to drop/pass should be governed by failopen setting) + if (verdictCode == 0) { + waf2Transaction.checkShouldInject(); + + if (waf2Transaction.shouldInspectResponse()) { + verdict = pending_response; + } else { + dbgTrace(D_WAAP) << "WAF VERDICT: " << verdictCode << " (\e[32mPASS\e[0m)"; + verdict = accept_response; + } + } else { + std::string message = (verdictCode == 1) ? " (\e[31mBLOCK\e[0m)" : " (\e[31mERROR!\e[0m)"; + dbgTrace(D_WAAP) << "WAF VERDICT: " << verdictCode << message; + verdict = drop_response; + } + + dbgTrace(D_WAAP) << "waapDecision() finished"; + return verdict; +} + +void +WaapComponent::Impl::finishTransaction(IWaf2Transaction& waf2Transaction) +{ + waf2Transaction.collectFoundPatterns(); + waf2Transaction.sendLog(); + ReportIS::Severity severity = waf2Transaction.computeEventSeverityFromDecision(); + validateFirstRequestForAsset(severity); + waapStateTable->deleteState(); +} + +void WaapComponent::Impl::validateFirstRequestForAsset(const ReportIS::Severity severity) +{ + static BasicRuleConfig empty_rule; + const BasicRuleConfig& rule_by_ctx = getConfigurationWithDefault( + empty_rule, + "rulebase", + "rulesConfig"); + if (rule_by_ctx.getAssetId().empty()) { + dbgWarning(D_WAAP) << "Failed to get rule base from context. Skipping sending notification."; + return; + } + + if (m_seen_assets_id.find(rule_by_ctx.getAssetId()) == m_seen_assets_id.end()) { + dbgTrace(D_WAAP) << "First request for asset id: '" << rule_by_ctx.getAssetId() + << "'. Sending notification"; + m_seen_assets_id.insert(rule_by_ctx.getAssetId()); + sendNotificationForFirstRequest( + rule_by_ctx.getAssetId(), + rule_by_ctx.getAssetName(), + severity + ); + } +} + +void WaapComponent::Impl::sendNotificationForFirstRequest( + const std::string& asset_id, + const std::string& asset_name, + const ReportIS::Severity severity +) +{ + dbgTrace(D_WAAP) << "Got first request for asset: '" << asset_name<< "' sending a notification"; + FirstRequestNotificationObject obj(asset_id, asset_name, severity); + I_MainLoop* mainloop = Singleton::Consume::by(); + mainloop->addOneTimeRoutine( + I_MainLoop::RoutineType::System, + [asset_name, obj]() + { + ReportMessaging( + "First request for asset '" + asset_name + "'", + ReportIS::AudienceTeam::WAAP, + obj, + ReportIS::Tags::WAF, + ReportIS::Notification::FIRST_REQUEST_FOR_ASSET + ); + }, + "Report WAAP asset first request inspection" + ); +} + +bool +WaapComponent::Impl::waf2_proc_start(const std::string& sigsFname, const std::string& scoresFname) +{ + // WAAP uses libxml library, which requires process-level initialization when process starts +#if 0 // TODO:: silence the error messages printed by libxml2 + xmlSetGenericErrorFunc(NULL, (xmlGenericErrorFunc)my_libxml2_err); + xmlSetStructuredErrorFunc(NULL, my_libxml_structured_err); +#endif + ::xmlInitParser(); + + return + Singleton::Consume::by()->initBasicWaapSigs(sigsFname, scoresFname); +} + +void +WaapComponent::Impl::waf2_proc_exit() +{ + ::xmlCleanupParser(); +} diff --git a/components/security_apps/waap/waap_component_impl.h b/components/security_apps/waap/waap_component_impl.h new file mode 100755 index 0000000..85ca964 --- /dev/null +++ b/components/security_apps/waap/waap_component_impl.h @@ -0,0 +1,88 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __WAAP_COMPONENT_IMPL_H__ +#define __WAAP_COMPONENT_IMPL_H__ + +#include "waap.h" +#include "config.h" +#include "table_opaque.h" +#include "i_transaction.h" +#include "waap_clib/DeepAnalyzer.h" +#include "waap_clib/WaapAssetState.h" +#include "waap_clib/WaapAssetStatesManager.h" +#include "reputation_features_agg.h" + +// WaapComponent implementation +class WaapComponent::Impl + : + public Listener, + public Listener, + public Listener, + public Listener, + public Listener, + public Listener, + public Listener, + public Listener +{ +public: + explicit Impl(); + virtual ~Impl(); + + void init(); + void fini(); + + std::string getListenerName() const override; + + EventVerdict respond(const NewHttpTransactionEvent &event) override; + EventVerdict respond(const HttpRequestHeaderEvent &event) override; + EventVerdict respond(const HttpRequestBodyEvent &event) override; + EventVerdict respond(const EndRequestEvent &) override; + + EventVerdict respond(const ResponseCodeEvent &event) override; + EventVerdict respond(const HttpResponseHeaderEvent &event) override; + EventVerdict respond(const HttpResponseBodyEvent &event) override; + EventVerdict respond(const EndTransactionEvent &) override; + +private: + void init(const std::string &sigs_file_path, const std::string &sigs_scores_file_path); + + EventVerdict waapDecisionAfterHeaders(IWaf2Transaction& waf2Transaction); + EventVerdict waapDecision(IWaf2Transaction& waf2Transaction); + void finishTransaction(IWaf2Transaction& waf2Transaction); + + bool waf2_proc_start(const std::string& sigsFname, const std::string& scoresFname); + void waf2_proc_exit(); + void validateFirstRequestForAsset(const ReportIS::Severity severity); + void sendNotificationForFirstRequest( + const std::string& asset_id, + const std::string& asset_name, + const ReportIS::Severity severity + ); + + EventVerdict pending_response; + EventVerdict accept_response; + EventVerdict drop_response; + WaapMetricWrapper waap_metric; + AssetsMetric assets_metric; + I_Table* waapStateTable; + // Count of transactions processed by this WaapComponent instance + uint64_t transactionsCount; + // instance of singleton classes + DeepAnalyzer deepAnalyzer; + WaapAssetStatesManager waapAssetStatesManager; + ReputationFeaturesAgg reputationAggregator; + std::unordered_set m_seen_assets_id; +}; + +#endif // __WAAP_COMPONENT_IMPL_H__ diff --git a/components/signal_handler/CMakeLists.txt b/components/signal_handler/CMakeLists.txt new file mode 100755 index 0000000..fe4a687 --- /dev/null +++ b/components/signal_handler/CMakeLists.txt @@ -0,0 +1,2 @@ +add_library(signal_handler signal_handler.cc) +target_compile_definitions(signal_handler PUBLIC) diff --git a/components/signal_handler/signal_handler.cc b/components/signal_handler/signal_handler.cc new file mode 100755 index 0000000..ab1f2b3 --- /dev/null +++ b/components/signal_handler/signal_handler.cc @@ -0,0 +1,479 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "signal_handler.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(use_unwind) +#if defined(alpine) || defined(PLATFORM_x86) +#define UNW_LOCAL_ONLY +#include +#include +#endif // defined(alpine) || defined(PLATFORM_x86) +#endif // defined(use_unwind) + +#if !defined(alpine) && !defined(arm32_musl) +#include +#endif // not alpine && not arm32_musl + +#include "debug.h" +#include "common.h" +#include "config.h" +#include "mainloop.h" +#include "report/log_rest.h" +#include "report/report.h" +#include "agent_core_utilities.h" + +#define stack_trace_max_len 64 + +using namespace std; +using namespace ReportIS; + +USE_DEBUG_FLAG(D_SIGNAL_HANDLER); + +class SignalHandler::Impl : Singleton::Provide::From +{ +public: + void + fini() + { + if (out_trace_file_fd != -1) close(out_trace_file_fd); + out_trace_file_fd = -1; + } + + void + dumpErrorReport(const string &error) override + { + ofstream export_error_file(trace_file_path); + export_error_file << error << endl; + } + + void + init() + { + addSignalHandlerRoutine(); + addReloadConfigurationRoutine(); + } + + Maybe> + getBacktrace() override + { + vector symbols; +#if defined(_UCLIBC_) || defined(arm32_musl) || !defined(use_unwind) + return genError("Could not print any backtrace entries using uclibc (backtrace_symbols not supported)"); +#else // not (_UCLIBC_ || arm32_musl) +#if defined(alpine) || defined(PLATFORM_x86) + unw_cursor_t cursor; + unw_context_t context; + + // Initialize cursor to current frame for local unwinding. + unw_getcontext(&context); + unw_init_local(&cursor, &context); + + char buf[1024]; + // Unwind frames one by one, going up the frame stack. + while (unw_step(&cursor) > 0) { + unw_word_t offset, pc; + unw_get_reg(&cursor, UNW_REG_IP, &pc); + if (pc == 0) break; + + char sym[256]; + if (unw_get_proc_name(&cursor, sym, sizeof(sym), &offset) == 0) { + char *nameptr = sym; + int status; + char *demangled = abi::__cxa_demangle(sym, nullptr, nullptr, &status); + if (status == 0) { + nameptr = demangled; + } + snprintf(buf, sizeof(buf), "(%s+0x%lx) [0x%lx]", nameptr, offset, pc); + free(demangled); + } else { + snprintf(buf, sizeof(buf), "-- error: unable to obtain symbol name for this frame"); + } + symbols.push_back(buf); + } +#else // not (alpine || PLATFORM_x86) + auto stack_trace_list = vector(stack_trace_max_len); + + uint trace_len = backtrace(stack_trace_list.data(), stack_trace_list.size()); + if (trace_len == 0 ) return genError("Could not find any backtrace entries in the current process"); + + char **trace_prints = backtrace_symbols(stack_trace_list.data(), trace_len); + if (trace_prints == nullptr) return genError("Could not convert backtrace entries to symbol strings"); + + symbols.reserve(trace_len); + for (uint i = 0; i < trace_len; ++i) { + symbols.emplace_back(trace_prints[i]); + } + free(trace_prints); + +#endif // alpine || PLATFORM_x86 +#endif // _UCLIBC_ + return symbols; + } + +private: + void + addSignalHandlerRoutine() + { + Singleton::Consume::by()->addOneTimeRoutine( + I_MainLoop::RoutineType::Offline, + [this] () + { + string service_name = "Unnamed Nano Service"; + if (Singleton::exists()) { + auto name = Singleton::Consume::by()->get( + "Service Name" + ); + if (name.ok()) service_name = *name; + } + string service_underscore_name = service_name; + replace(service_underscore_name.begin(), service_underscore_name.end(), ' ', '_'); + + trace_file_path = getConfigurationWithDefault( + "/var/log/nano_agent/trace_export_files/" + service_underscore_name + "_trace_file.dbg", + "SignalHandler", + "outputFilePath" + ); + ifstream in_trace_file(trace_file_path); + + if (in_trace_file.peek() != ifstream::traits_type::eof()) { + stringstream buffer; + buffer << in_trace_file.rdbuf(); + if (buffer.str() != " " && buffer.str() != "\n") { + const boost::regex reg("(\\+0x[A-z0-9]*)|( [0x[A-z0-9]*])"); + const string fixed_trace_str = NGEN::Regex::regexReplace( + __FILE__, + __LINE__, + buffer.str(), + reg, + "" + ); + generateLog(fixed_trace_str); + dbgInfo(D_SIGNAL_HANDLER) + << "Service started after crash ERROR: " + << endl + << fixed_trace_str; + } + } + + in_trace_file.close(); + + ofstream out_trace_file(trace_file_path, ofstream::out | ofstream::trunc); + out_trace_file.close(); + + setSignalHanlders(); + }, + "Send crash trace report" + ); + } + + void + generateLog(const string &trace_file_data) + { + auto i_time = Singleton::Consume::by(); + chrono::microseconds curr_time = i_time!=nullptr ? i_time->getWalltime() : chrono::microseconds(0); + + if (!Singleton::exists()) return; + + AudienceTeam audience_team = AudienceTeam::NONE; + if (Singleton::exists()) { + auto team = Singleton::Consume::by()->get("Audience Team"); + if (team.ok()) audience_team = *team; + } + + set tags; + Report message_to_fog( + "Nano service startup after crash", + curr_time, + Type::EVENT, + Level::LOG, + LogLevel::ERROR, + Audience::INTERNAL, + audience_team, + Severity::HIGH, + Priority::HIGH, + chrono::seconds(0), + LogField("agentId", Singleton::Consume::by()->getAgentId()), + tags, + Tags::INFORMATIONAL + ); + + message_to_fog << LogField("eventMessage", trace_file_data); + + string fog_signalHandler_uri = getConfigurationWithDefault( + "/api/v1/agents/events", + "SignalHandler", + "fogSignalHandlerURI" + ); + + LogRest signalHandler_client_rest(message_to_fog); + + Singleton::Consume::by()->sendObjectWithPersistence( + signalHandler_client_rest, + I_Messaging::Method::POST, + fog_signalHandler_uri, + "", + true, + MessageTypeTag::REPORT + ); + + dbgInfo(D_SIGNAL_HANDLER) << "Sent crash log to fog" << endl; + } + + void + setSignalHanlders() + { + out_trace_file_fd = open(trace_file_path.c_str(), O_CREAT | O_RDWR, S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH); + int errno_copy = errno; + if (out_trace_file_fd < 0) { + dbgError(D_SIGNAL_HANDLER) + << "Failed to open signal handler backtrace file. Path: " + << trace_file_path + << ", Errno: " + << to_string(errno_copy) + << ", Error: " + << strerror(errno_copy); + } + + static const vector signals = { + SIGABRT, + SIGKILL, + SIGQUIT, + SIGINT, + SIGTERM, + SIGSEGV, + SIGBUS, + SIGILL, + SIGFPE, + SIGPIPE, + SIGUSR2 + }; + + for (int sig : signals) { + signal(sig, signalHandlerCB); + } + } + +// LCOV_EXCL_START Reason: Cannot crash unitest or send signal during execution + static bool + writeData(const char *data, uint32_t len) + { + uint32_t bytes_sent = 0; + while (bytes_sent < len) { + int res = write(out_trace_file_fd, data + bytes_sent, len - bytes_sent); + if (res <= 0) return false; + + bytes_sent += res; + } + + return true; + } + + static void + signalHandlerCB(int _signal) + { + const char *signal_name = ""; + char signal_num[3]; + switch(_signal) { + case SIGABRT: { + signal_name = "SIGABRT"; + fini_signal_flag = true; + return; + } + case SIGKILL: { + signal_name = "SIGKILL"; + fini_signal_flag = true; + return; + } + case SIGQUIT: { + signal_name = "SIGQUIT"; + fini_signal_flag = true; + return; + } + case SIGINT: { + signal_name = "SIGINT"; + fini_signal_flag = true; + return; + } + case SIGTERM: { + signal_name = "SIGTERM"; + fini_signal_flag = true; + return; + } + case SIGSEGV: { + signal_name = "SIGSEGV"; + break; + } + case SIGBUS: { + signal_name = "SIGBUS"; + break; + } + case SIGILL: { + signal_name = "SIGILL"; + break; + } + case SIGFPE: { + signal_name = "SIGFPE"; + break; + } + case SIGPIPE: { + signal_name = "SIGPIPE"; + return; + } + case SIGUSR2: { + reload_settings_flag = true; + return; + } + } + + if (out_trace_file_fd == -1) exit(_signal); + + for (uint i = 0; i < sizeof(signal_num); ++i) { + uint placement = sizeof(signal_num) - 1 - i; + signal_num[placement] = _signal%10 + '0'; + _signal /= 10; + } + const char *signal_error_prefix = "Caught signal "; + writeData(signal_error_prefix, strlen(signal_error_prefix)); + writeData(signal_num, sizeof(signal_num)); + if (strlen(signal_name)) { + const char *open_braces = "("; + writeData(open_braces, strlen(open_braces)); + writeData(signal_name, strlen(signal_name)); + const char *close_braces = ")\n"; + writeData(close_braces, strlen(close_braces)); + } + + printStackTrace(); + + close(out_trace_file_fd); + out_trace_file_fd = -1; + + exit(_signal); + } + + static void + printStackTrace() + { + if (out_trace_file_fd == -1) return; + + const char *stack_trace_title = "Stack trace:\n"; + writeData(stack_trace_title, strlen(stack_trace_title)); + +#if defined(_UCLIBC_) || defined(arm32_musl) || !defined(use_unwind) + const char *uclibc_error = + "Could not print any backtrace entries using uclibc (backtrace_symbols not supported)\n"; + writeData(uclibc_error, strlen(uclibc_error)); + return; +#else // not (_UCLIBC_ || arm32_musl) +#ifdef alpine + unw_cursor_t cursor; + unw_context_t uc; + unw_getcontext(&uc); + if (unw_init_local(&cursor, &uc) < 0) { + const char *unw_init_local_error = "unw_init_local failed!\n"; + writeData(unw_init_local_error, strlen(unw_init_local_error)); + return; + } + + char name[256]; + unw_word_t ip, sp, off; + for (uint i = 0 ; i < stack_trace_max_len ; i++) { + unw_get_reg(&cursor, UNW_REG_IP, &ip); + unw_get_reg(&cursor, UNW_REG_SP, &sp); + + if (unw_get_proc_name(&cursor, name, sizeof(name), &off) == 0) { + const char *open_braces = "<"; + writeData(open_braces, strlen(open_braces)); + writeData(name, strlen(name)); + const char *close_braces = ">\n"; + writeData(close_braces, strlen(close_braces)); + } + + + if (unw_step(&cursor) <= 0) return; + } +#else // not alpine + void *stack_trace_list[stack_trace_max_len]; + + uint actual_trace_len = backtrace(stack_trace_list, stack_trace_max_len); + if (actual_trace_len == 0 ) { + const char *no_bt_found_error = "Could not find any backtrace entries in the current process\n"; + writeData(no_bt_found_error, strlen(no_bt_found_error)); + return; + } + + backtrace_symbols_fd(stack_trace_list, actual_trace_len, out_trace_file_fd); +#endif // alpine +#endif // _UCLIBC_ || arm32_musl + } +// LCOV_EXCL_STOP + + void + addReloadConfigurationRoutine() + { + Singleton::Consume::by()->addOneTimeRoutine( + I_MainLoop::RoutineType::System, + [&] () + { + while (true) { + if (reload_settings_flag == true) { + reload_settings_flag = false; + if (reloadConfiguration("")) { + dbgInfo(D_SIGNAL_HANDLER) << "Reloaded configuration"; + } else { + dbgWarning(D_SIGNAL_HANDLER) << "Failed to reload configuration"; + } + } + Singleton::Consume::by()->yield(chrono::seconds(1)); + } + }, + "Reload configuration signal handler" + ); + } + + static string trace_file_path; + static bool reload_settings_flag; + static int out_trace_file_fd; +}; + +string SignalHandler::Impl::trace_file_path; +bool SignalHandler::Impl::reload_settings_flag = false; +int SignalHandler::Impl::out_trace_file_fd = -1; + +SignalHandler::SignalHandler() : Component("SignalHandler"), pimpl(make_unique()) {} +SignalHandler::~SignalHandler() {} + +void SignalHandler::init() { pimpl->init(); } + +void +SignalHandler::preload() +{ + registerExpectedConfiguration("SignalHandler", "outputFilePath"); + registerExpectedConfiguration("SignalHandler", "fogSignalHandlerURI"); +} diff --git a/components/utils/CMakeLists.txt b/components/utils/CMakeLists.txt new file mode 100644 index 0000000..3863672 --- /dev/null +++ b/components/utils/CMakeLists.txt @@ -0,0 +1,2 @@ +add_subdirectory(ip_utilities) +add_subdirectory(pm) diff --git a/components/utils/ip_utilities/CMakeLists.txt b/components/utils/ip_utilities/CMakeLists.txt new file mode 100755 index 0000000..a6dd7e2 --- /dev/null +++ b/components/utils/ip_utilities/CMakeLists.txt @@ -0,0 +1 @@ +add_library(ip_utilities ip_utilities.cc) diff --git a/components/utils/ip_utilities/ip_utilities.cc b/components/utils/ip_utilities/ip_utilities.cc new file mode 100644 index 0000000..c467516 --- /dev/null +++ b/components/utils/ip_utilities/ip_utilities.cc @@ -0,0 +1,347 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "ip_utilities.h" + +#include "connkey.h" + +using namespace std; + +// LCOV_EXCL_START Reason: temporary until we add relevant UT until 07/10 +bool +operator<(const IpAddress &this_ip_addr, const IpAddress &other_ip_addr) +{ + if (this_ip_addr.ip_type < other_ip_addr.ip_type) return true; + if (this_ip_addr.ip_type == IP_VERSION_4) return this_ip_addr.addr4_t.s_addr < other_ip_addr.addr4_t.s_addr; + return memcmp(&this_ip_addr.addr6_t, &other_ip_addr.addr6_t, sizeof(struct in6_addr)) < 0; +} + +bool +operator==(const IpAddress &this_ip_addr, const IpAddress &other_ip_addr) +{ + if (this_ip_addr.ip_type != other_ip_addr.ip_type) return false; + if (this_ip_addr.ip_type == IP_VERSION_4) return this_ip_addr.addr4_t.s_addr == other_ip_addr.addr4_t.s_addr; + return memcmp(&this_ip_addr.addr6_t, &other_ip_addr.addr6_t, sizeof(struct in6_addr)) == 0; +} +// LCOV_EXCL_STOP + +Maybe> +extractAddressAndMaskSize(const string &cidr) +{ + size_t delimiter_pos = cidr.find("/"); + if (delimiter_pos == string::npos) return genError("provided value is not in CIDR notation: " + cidr); + string address = cidr.substr(0, delimiter_pos); + string mask_size = cidr.substr(delimiter_pos + 1, cidr.size() - delimiter_pos - 1); + try { + return make_pair(address, stoi(mask_size)); + } catch(...) { + return genError("failed to cast provided value to integer: " + mask_size); + } + return genError("failed to parse provided string as a CIDR: " + cidr); +} + +template +pair +applyMaskOnAddress(const vector &oct, Integer mask) +{ + Integer start = (oct[0] | oct[1] | oct[2] | oct[3]) & mask; + Integer end = (oct[0] | oct[1] | oct[2] | oct[3]) | (~mask); + return make_pair(start, end); +} + +Maybe> +createRangeFromCidrV4(const pair &cidr_values) +{ + string address = cidr_values.first; + int mask_size = cidr_values.second; + vector oct; + for (int i=3; i>=0; i--) { + size_t delimiter_pos = address.find("."); + string oct_str = address.substr(0, delimiter_pos); + try { + oct.push_back(static_cast(stoul(oct_str)) << (i * 8)); + } catch (...) { + return genError("failed to cast provided value to integer: " + oct_str); + } + if ((i == 0) != (delimiter_pos == string::npos)) { + return genError("provided value is not in a correct ipv4 structure: " + makeSeparatedStr(oct, ".")); + } + address.erase(0, delimiter_pos + 1); + } + + unsigned int mask = 0xffffffff; + mask <<= (32 - mask_size); + + unsigned int start, end; + tie(start, end) = applyMaskOnAddress(oct, mask); + + auto construct_address = [](unsigned int value) + { + stringstream address_stream; + for (int i = 3; i >= 0; i--) { + address_stream << ((value >> (i * 8)) & 0xff) << (i > 0 ? "." : ""); + } + return address_stream.str(); + }; + + return make_pair(construct_address(start), construct_address(end)); +} + +// LCOV_EXCL_START Reason: it is tested, but for some reason coverage doesn't catch it +Maybe> +createRangeFromCidrV6(const pair &cidr_values) +{ + string address = cidr_values.first; + int mask_size = cidr_values.second; + // fill compressed zeros + struct in6_addr v6; + if (inet_pton(AF_INET6, address.c_str(), &v6) == -1) { + return genError("faild to convert provided value to ipv6: " + address); + }; + struct in6_addr *addr = &v6; + vector oct_from_str; + for (int i=0; i<15; i+=2){ + char hex[8]; + unsigned int num; + sprintf(hex, "%02x%02x", static_cast(addr->s6_addr[i]), static_cast(addr->s6_addr[i+1])); + sscanf(hex, "%x", &num); + oct_from_str.push_back(num); + } + + uint64_t mask = 0xffffffffffffffff; + function construct_address; + int oct_offset; + + if (mask_size > 64) { + oct_offset = 7; + mask <<= (128 - mask_size); + construct_address = [oct_from_str](uint64_t value, bool is_start) + { + (void)is_start; + stringstream address_stream; + for (int i = 0; i < 4; i++) { + address_stream << hex << oct_from_str[i] << ":"; + } + for (int i = 3; i >= 0; i--) { + address_stream << hex << (unsigned int)((value >> (i * 16)) & 0xffff) << (i > 0 ? ":" : ""); + } + return address_stream.str(); + }; + } else { + oct_offset = 3; + mask <<= (64 - mask_size); + construct_address = [](uint64_t value, bool is_start) + { + stringstream address_stream; + for (int i = 3; i >= 0; i--) { + address_stream << hex << (unsigned int)((value >> (i * 16)) & 0xffff) << ":"; + } + address_stream << (is_start ? "0:0:0:0" : "ffff:ffff:ffff:ffff"); + return address_stream.str(); + }; + } + + uint64_t start, end; + vector oct; + for (int i = 3; i >= 0; i--) { + oct.push_back(static_cast(oct_from_str[oct_offset - i]) << (i * 16)); + } + tie(start, end) = applyMaskOnAddress(oct, mask); + return make_pair( + construct_address(start, true), + construct_address(end, false) + ); +} +// LCOV_EXCL_STOP + +namespace IPUtilities { +Maybe> +getInterfaceIPs() +{ + struct ifaddrs *if_addr_list = nullptr; + if (getifaddrs(&if_addr_list) == -1) { + return genError(string("Failed to get interface IP's. Error: ") + strerror(errno)); + } + + map interface_ips; + for (struct ifaddrs *if_addr = if_addr_list; if_addr != nullptr; if_addr = if_addr->ifa_next) { + if (if_addr->ifa_addr == nullptr) continue; + if (if_addr->ifa_addr->sa_family != AF_INET && if_addr->ifa_addr->sa_family != AF_INET6) continue; + + char address_buffer[INET6_ADDRSTRLEN] = { '\0' }; + if (if_addr->ifa_addr->sa_family == AF_INET) { + struct in_addr addr = reinterpret_cast(if_addr->ifa_addr)->sin_addr; + inet_ntop(AF_INET, &addr, address_buffer, INET_ADDRSTRLEN); + string address_string(address_buffer); + if (address_string.find("127.0.0.1") != string::npos) continue; + + IpAddress ip_addr; + ip_addr.ip_type = IP_VERSION_4; + memcpy(&ip_addr.ip.ipv4, &addr, sizeof(ip_addr.ip.ipv4)); + interface_ips.emplace(ip_addr, address_string); + } else { + struct in6_addr addr = reinterpret_cast(if_addr->ifa_addr)->sin6_addr; + inet_ntop(AF_INET6, &addr, address_buffer, INET6_ADDRSTRLEN); + string address_string(address_buffer); + if (address_string.find("::1") != string::npos) continue; + + IpAddress ip_addr; + ip_addr.ip_type = IP_VERSION_6; + memcpy(&ip_addr.ip.ipv6, &addr, sizeof(ip_addr.ip.ipv6)); + interface_ips.emplace(ip_addr, address_string); + } + } + + if (if_addr_list != nullptr) freeifaddrs(if_addr_list); + + return interface_ips; +} + +Maybe> +createRangeFromCidr(const string &cidr) +{ + auto cidr_values = extractAddressAndMaskSize(cidr); + if (!cidr_values.ok()) return genError("Failed to create range from Cidr: " + cidr_values.getErr()); + return cidr.find(".") != string::npos + ? createRangeFromCidrV4(cidr_values.unpack()) + : createRangeFromCidrV6(cidr_values.unpack()); +} + +bool +isIpAddrInRange(const IPRange &rule_ip_range, const IpAddress &ip_addr) +{ + IpAddress min_ip = rule_ip_range.start; + IpAddress max_ip = rule_ip_range.end; + + if (ip_addr.ip_type == IP_VERSION_4) { + if (max_ip.ip_type != IP_VERSION_4) return 0; + return + memcmp(&ip_addr.ip.ipv4, &min_ip.ip.ipv4, sizeof(struct in_addr)) >= 0 && + memcmp(&ip_addr.ip.ipv4, &max_ip.ip.ipv4, sizeof(struct in_addr)) <= 0; + } + if (ip_addr.ip_type == IP_VERSION_6) { + if (max_ip.ip_type != IP_VERSION_6) return 0; + return + memcmp(&ip_addr.ip.ipv6, &min_ip.ip.ipv6, sizeof(struct in6_addr)) >= 0 && + memcmp(&ip_addr.ip.ipv6, &max_ip.ip.ipv6, sizeof(struct in6_addr)) <= 0; + } + return 0; +} + +string +IpAddrToString(const IpAddress &address) +{ + if (address.ip_type == IP_VERSION_6) { + char ip_str[INET6_ADDRSTRLEN]; + struct sockaddr_in6 sa6; + + sa6.sin6_family = AF_INET6; + sa6.sin6_addr = address.ip.ipv6; + + inet_ntop(AF_INET6, &(sa6.sin6_addr), ip_str, INET6_ADDRSTRLEN); + return move(string(ip_str)); + } + + char ip_str[INET_ADDRSTRLEN]; + struct sockaddr_in sa; + + sa.sin_family = AF_INET; + sa.sin_addr = address.ip.ipv4; + + inet_ntop(AF_INET, &(sa.sin_addr), ip_str, INET_ADDRSTRLEN); + return move(string(ip_str)); +} + +IpAddress +createIpFromString(const string &ip_string) +{ + IpAddress res_address = {0, IP_VERSION_ANY}; + if (ip_string == "any") return res_address; + auto maybe_ip_addr = IPAddr::createIPAddr(ip_string); + if (!maybe_ip_addr.ok()) { + return res_address; + } + IPAddr ip_addr = maybe_ip_addr.unpack(); + res_address.ip_type = static_cast(ip_addr.getType()); + if (ip_addr.getType() == IPType::V4) { + res_address.addr4_t = ip_addr.getIPv4(); + } else { + res_address.addr6_t = ip_addr.getIPv6(); + } + return res_address; +} + +IpAddress +ConvertToIpAddress(const IPAddr &addr) { + IpAddress address; + switch (addr.getType()) { + case IPType::UNINITIALIZED: { + address.addr4_t = {0}; + address.ip_type = IP_VERSION_ANY; + break; + } + case IPType::V4: { + address.addr4_t = addr.getIPv4(); // reference to a local variable ? + address.ip_type = IP_VERSION_4; + break; + } + case IPType::V6: { + address.addr6_t = addr.getIPv6(); + address.ip_type = IP_VERSION_6; + break; + } + default: + dbgAssert(false) << "Unsupported IP type"; + } + return address; +} + +IpAttrFromString::operator Maybe() +{ + auto ip_addr = IPAddr::createIPAddr(data); + if (!ip_addr.ok()) return genError("Could not create IP address. Error: " + ip_addr.getErr()); + return ConvertToIpAddress(ip_addr.unpackMove()); +} + +IpAttrFromString::operator Maybe() +{ + int value; + try { + value = stoi(data); + } catch (...) { + return genError("provided value is not a legal number. Value: " + data); + } + + if (value > static_cast(UINT8_MAX) || value < 0) { + return genError("provided value is not a legal ip protocol number. Value: " + data); + } + + return static_cast(value); +} + +IpAttrFromString::operator Maybe() +{ + int value; + try { + value = stoi(data); + } catch (...) { + return genError("provided value is not a legal number. Value: " + data); + } + + if (value > static_cast(UINT16_MAX) || value < 0) { + return genError("provided value is not a legal port number. Value: " + data); + } + + return static_cast(value); +} +} diff --git a/components/utils/pm/CMakeLists.txt b/components/utils/pm/CMakeLists.txt new file mode 100644 index 0000000..a953975 --- /dev/null +++ b/components/utils/pm/CMakeLists.txt @@ -0,0 +1,3 @@ +add_library(pm general_adaptor.cc kiss_hash.cc kiss_patterns.cc kiss_pm_stats.cc kiss_thin_nfa.cc kiss_thin_nfa_analyze.cc kiss_thin_nfa_build.cc kiss_thin_nfa_compile.cc pm_adaptor.cc pm_hook.cc debugpm.cc) + +add_subdirectory(pm_ut) diff --git a/components/utils/pm/debugpm.cc b/components/utils/pm/debugpm.cc new file mode 100755 index 0000000..8e2fbdb --- /dev/null +++ b/components/utils/pm/debugpm.cc @@ -0,0 +1,63 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "debug.h" +#include +#include +#include +#include +#include +#include +#include "sasal.h" + +using namespace std; + +USE_DEBUG_FLAG(D_PM); + +SASAL_START // Multiple Pattern Matcher +// Helper class for printing C format string +class CFmtPrinter +{ +public: + char buf[500]; // Length limit. + explicit CFmtPrinter(const char *fmt, va_list va) + { + vsnprintf(buf, sizeof(buf), fmt, va); + buf[sizeof(buf)-1] = '\0'; + } +}; + +static ostream & +operator<<(ostream &os, const CFmtPrinter &p) +{ + return os << p.buf; +} + +void +panicCFmt(const string &func, uint line, const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + Debug("PM", func, line).getStreamAggr() << CFmtPrinter(fmt, va); + va_end(va); +} + +void +debugPrtCFmt(const char *func, uint line, const char *fmt, ...) +{ + va_list va; + va_start(va, fmt); + Debug("PM", func, line, Debug::DebugLevel::TRACE, D_PM).getStreamAggr() << CFmtPrinter(fmt, va); + va_end(va); +} +SASAL_END diff --git a/components/utils/pm/debugpm.h b/components/utils/pm/debugpm.h new file mode 100755 index 0000000..9d4ce48 --- /dev/null +++ b/components/utils/pm/debugpm.h @@ -0,0 +1,39 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __DEBUGPM_H__ +#define __DEBUGPM_H__ + +#include +#include + +#include "debug.h" + +// Assertions + +// C-style BC functions (e.g. for PM). +void debugPrtCFmt(const std::string &func, uint line, const char *fmt, ...) __attribute__((format (printf, 3, 4))); +#define debugCFmt(flag, fmt, ...) \ + if (!Debug::isDebugSet(flag)) \ + { \ + } else \ + debugPrtCFmt(__FUNCTION__, __LINE__, fmt, ##__VA_ARGS__) + +void panicCFmt(const std::string &func, uint line, const char *fmt, ...) __attribute__((format (printf, 3, 4))); +#define assertCondCFmt(cond, fmt, ...) \ + if (CP_LIKELY(cond)) \ + { \ + } else \ + panicCFmt(__FUNCTION__, __LINE__, fmt, ##__VA_ARGS__) + +#endif // __DEBUGPM_H__ diff --git a/components/utils/pm/general_adaptor.cc b/components/utils/pm/general_adaptor.cc new file mode 100644 index 0000000..33939b4 --- /dev/null +++ b/components/utils/pm/general_adaptor.cc @@ -0,0 +1,65 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "general_adaptor.h" +#include +#include "sasal.h" + +SASAL_START // Multiple Pattern Matcher +void fw_kfree(void *addr, CP_MAYBE_UNUSED size_t size, CP_MAYBE_UNUSED const char *caller) +{ + free(addr); + return; +} + +void *fw_kmalloc(size_t size, CP_MAYBE_UNUSED const char *caller) +{ + return malloc(size); +} + +void *fw_kmalloc_ex(size_t size, CP_MAYBE_UNUSED const char *caller, CP_MAYBE_UNUSED int flags) +{ + return malloc(size); +} + +void *fw_kmalloc_sleep(size_t size, CP_MAYBE_UNUSED const char *caller) +{ + return malloc(size); +} + +void *kiss_pmglob_memory_kmalloc_ex_( + u_int size, + CP_MAYBE_UNUSED const char *caller, + CP_MAYBE_UNUSED int flags, + CP_MAYBE_UNUSED const char *file, + CP_MAYBE_UNUSED int line) +{ + return malloc(size); +} + +void *kiss_pmglob_memory_kmalloc_ex(u_int size, CP_MAYBE_UNUSED const char *caller, CP_MAYBE_UNUSED int flags) +{ + return malloc(size); +} + +void *kiss_pmglob_memory_kmalloc(u_int size, CP_MAYBE_UNUSED const char *caller) +{ + return malloc(size); +} + +void kiss_pmglob_memory_kfree(void *addr, CP_MAYBE_UNUSED size_t size, CP_MAYBE_UNUSED const char *caller) +{ + free(addr); + return; +} +SASAL_END diff --git a/components/utils/pm/general_adaptor.h b/components/utils/pm/general_adaptor.h new file mode 100644 index 0000000..f489782 --- /dev/null +++ b/components/utils/pm/general_adaptor.h @@ -0,0 +1,80 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __general_adaptor_h__ +#define __general_adaptor_h__ + +#include "stdint.h" +#include +#include +#include +#include "common.h" +#include "debug.h" +#include "debugpm.h" + +typedef unsigned int u_int; +typedef unsigned char u_char; +typedef unsigned short u_short; +typedef bool boolean_cpt; +typedef bool BOOL; +typedef uint64_t u_int64; + +#define TRUE true +#define FALSE false + +#define CP_INLINE inline +#define CP_CACHELINE_SIZE 64 +#define CP_CACHELINE_ALIGNED __attribute__((__aligned__(CP_CACHELINE_SIZE))) +#define CP_MAYBE_UNUSED CP_UNUSED + +#define KISS_OFFSETOF(str_name, field_name) offsetof(str_name, field_name) + +#define KISS_ASSERT_COMPILE_TIME(cond) extern int __kiss_assert_dummy[(cond)?1:-1] + +#define KISS_ASSERT_PERF(...) +#define ASSERT_LOCKED +#define kiss_multik_this_instance_num (0) + +typedef enum { + KISS_ERROR = -1, + KISS_OK = 0 +} kiss_ret_val; + +#define KISS_ASSERT assertCondCFmt +#define KISS_ASSERT_CRASH assertCondCFmt + +#define FW_KMEM_SLEEP 0 + +#define herror(a, b, ...) + +#define kdprintf printf +#define kdprintf_no_prefix printf + + +void fw_kfree(void *addr, size_t size, const char *caller); +void *fw_kmalloc(size_t size, const char *caller); +void *fw_kmalloc_ex(size_t size, const char *caller, int flags); +void *fw_kmalloc_sleep(size_t size, const char *caller); +void *kiss_pmglob_memory_kmalloc_ex_(u_int size, const char *caller, int flags, const char *file, int line); +void *kiss_pmglob_memory_kmalloc_ex(u_int size, const char *caller, int flags); +void *kiss_pmglob_memory_kmalloc(u_int size, const char *caller); +void kiss_pmglob_memory_kfree(void *addr, size_t size, const char *caller); + +#define ENUM_SET_FLAG(e, flag) e = static_cast(((u_int)e | (u_int)flag)) +#define ENUM_UNSET_FLAG(e, flag) e = static_cast(((u_int)e & (~(u_int)flag))) + +#define MAX(x, y) (((x)>(y))?(x):(y)) +#define MIN(x, y) (((x)<(y))?(x):(y)) + + +#endif // __general_adaptor_h__ diff --git a/components/utils/pm/kiss_hash.cc b/components/utils/pm/kiss_hash.cc new file mode 100644 index 0000000..13755ff --- /dev/null +++ b/components/utils/pm/kiss_hash.cc @@ -0,0 +1,1783 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "general_adaptor.h" +#include "sasal.h" + +SASAL_START // Multiple Pattern Matcher +#ifndef KERNEL + +#if defined(VXWORKS) || defined(freebsd) || defined (solaris2) +#include +#elif defined (ARMON) +#include +// #include +#elif defined(SYS_PSOS) +#include +#else +#include +#endif + +#endif // KERNEL + +#include "kiss_hash.h" + +// we provide hash_craete, hash_create_with_destr function implementations +#undef kiss_hash_create +#undef kiss_hash_create_with_destr + +#ifndef NULL +#define NULL 0 +#endif +#ifndef HASH_DEFAULT_SIZE +#define HASH_DEFAULT_SIZE 1024 +#endif + +static void KissHashResizeMode_reset_parameters(KissHashResizeMode *resize_mode); +static void KissHashResizeMode_set_default_parameters(KissHashResizeMode *resize_mode); +static int KissHashResizeMode_verify_method(const KissHashResizeMode *resize_mode); +static int KissHashResizeMode_verify_value(const KissHashResizeMode *resize_mode); +static int KissHashResizeMode_verify_trigger_ratio(const KissHashResizeMode *resize_mode); +static int KissHashResizeMode_verify_direction(const KissHashResizeMode *resize_mode); +static int kiss_hash_do_resize(kiss_hash_t hp, const KissHashResizeMode *resize_mode); +static boolean_cpt kiss_hash_resize_check_for_resize(kiss_hash_t hp, KissHashResizeDirection direction); + +struct _KissHashResizeMode { + u_int max_size; + KissHashResizeMethod method; + KissHashResizeDirection direction; + u_int value; + u_int trigger_ratio; + HashResizeCb_t cb; +}; + +struct kiss_hash { + char *file; // source file name where hash was created + int line; // line number where hash was created + int hash_index; + struct kiss_hashent **h_tab; + int h_nelements; + int h_sz; + int h_orig_size; + KissHashResizeMode h_resize_mode; + int h_dodestr; + uintptr_t (*h_keyfunc)(const void *key, void *info); + int (*h_keycmp)(const void *key1, const void *key2, void *info); + void (*h_val_destr)(void *val); + void (*h_key_destr)(void *key); + void *h_info; +}; + +struct kiss_hash_iter { + kiss_hash_t hash; + int slot; + struct kiss_hashent *pntr; +}; + +// pointers to created hash tables +#ifdef HASH_DEBUG +#define MAX_HASHES 1024 +static kiss_hash_t kiss_hashes[MAX_HASHES]; +static int kiss_curr_hash = 0; +static int do_kiss_hash_debug = 0; +static int kiss_checked_env = 0; + +static void dbg_register_hash(kiss_hash_t hash, int line, const char *file) { + + hash->line = line; + hash->file = (char *) file; + + if (kiss_checked_env && !do_kiss_hash_debug) + return; + + if (!kiss_checked_env) { + if (getenv("CP_HASH_DEBUG")) + do_kiss_hash_debug = 1; + kiss_checked_env = 1; + } + + MtBeginCS(); + if (kiss_curr_hash != MAX_HASHES) { + kiss_hashes[kiss_curr_hash] = hash; + hash->hash_index = kiss_curr_hash++; + } + else + hash->hash_index = -1; + MtEndCS(); + +} + +static void dbg_deregister_hash(kiss_hash_t hash) { + + if ((kiss_checked_env && !do_kiss_hash_debug) || !kiss_checked_env) + return; + + if (hash->hash_index == -1) + return; + + MtBeginCS(); + if (kiss_curr_hash > 0) { + kiss_curr_hash--; + kiss_hashes[hash->hash_index] = kiss_hashes[kiss_curr_hash]; + } + MtEndCS(); + +} + + +// @name Hash functions +// +// +// +// Debug single hash. + +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// This function calculates and prints the following statistics: +// \begin{itemize} +// \item hash pointer +// \item file name and line number where \Ref{hash_create} or \Ref{hash_create_with_destr} was called +// \item number of elements in hash +// \item number of slots in hash - hash size +// \item size in bytes of memory occupied by hash maintenance structures +// \item slot utilzation - percentage of hash slots used to store elements +// \item average number of lookups - average length of lists of elements +// \end{itemize} +// +// @param hash pointer to hash +// @return size in bytes of memory occupied by hash maintenance structures. +// @see kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, kiss_hash_delete, +// kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, +// kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp, +// kiss_hash_debug_all +int kiss_hash_debug(kiss_hash_t hash) { + + int slot, used_slots=0; + double slot_utilization, avg_lookup; + int kiss_hash_size = hash->h_sz; + int mem_size = + sizeof(struct kiss_hash) + + kiss_hash_size * sizeof(struct kiss_hashent*) + + hash->h_nelements*sizeof(struct kiss_hashent); + + // check slot utilization + for (slot=0; sloth_tab[slot]) used_slots++; + } + + slot_utilization = (double) used_slots/kiss_hash_size; + avg_lookup = (used_slots) ? (double) hash->h_nelements/used_slots : 0; + + error( + 0, + 0, + "hash 0x%x created in %s:%d : nelements=%d kiss_hash_size=%d " + "mem_size=%d slot_utilzation %f (%d of %d) avg lookup %f", + hash, + hash->file, + hash->line, + hash->h_nelements, + kiss_hash_size, + mem_size, + slot_utilization, + used_slots, + kiss_hash_size, + avg_lookup + ); + + return mem_size; +} + + +// Debug single hash. +// +// \begin{description} +// \item[ MT-Level: ] Safe +// \end{description} +// +// Iterates a list of all hash tables craeted in the current process and +// for each hash calls function \Ref{kiss_hash_debug}. In addition the total +// memory usage of hash maintenance structures is printed. +// +// @see kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, kiss_hash_delete, +// kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, +// kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp, kiss_hash_debug +void kiss_hash_debug_all() { + int i, total_mem_size=0; + + if ((kiss_checked_env && !do_kiss_hash_debug) || !kiss_checked_env) return; + + MtBeginCS(); + error(0, 0, "[%s] Hash Debug", ltime(0)); + for (i=0; ih_dodestr) { + H_DESTR(hp->h_val_destr, he->val); + H_DESTR(hp->h_key_destr, he->key); + } +} + + +// Number of hash elements. +// +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hash hash table +// @return number of elements +// @see kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, kiss_hash_delete, +// kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, +// kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp +int +kiss_hash_nelements(kiss_hash_t hash) +{ + return hash->h_nelements; +} + + +// Hash size. +// +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hash hash table +// @return Size of hash +// @see kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, kiss_hash_delete, +// kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, +// kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp +int +kiss_hash_get_size(kiss_hash_t hash) +{ + return hash->h_sz + 1; // In hash create we decrease by 1 the application size +} + + +// Hash orignal size. +// +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hash hash table +// @return Original size of hash (for hash tables with dynamic size). +// @see kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, kiss_hash_delete, +// kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, +// kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp +int kiss_hash_orig_size(kiss_hash_t hash) +{ + return hash->h_orig_size + 1; // In hash create we decrease by 1 the application size +} + +static kiss_hash_t +kiss_hash_create_do(size_t hsize, + hkeyfunc_t keyfunc, + hcmpfunc_t keycmp, + void *info, + boolean_cpt do_kernel_sleep) +{ + extern int roundtwo(int n); + + kiss_hash_t hp; + + if (hsize == 0) hsize = HASH_DEFAULT_SIZE; + + hsize = roundtwo(hsize); + + if(do_kernel_sleep) { + hp = (kiss_hash_t)kiss_pmglob_memory_kmalloc_ex_( + sizeof(struct kiss_hash), + "kiss_hash_create", + FW_KMEM_SLEEP, + __FILE__, + __LINE__ + ); + } else { + hp = (kiss_hash_t)kiss_pmglob_memory_kmalloc((sizeof(struct kiss_hash)), "kiss_hash_create"); + } + + if (hp == NULL) return NULL; + memset(hp, 0, sizeof(struct kiss_hash)); + + if(do_kernel_sleep) { + hp->h_tab = (struct kiss_hashent **)kiss_pmglob_memory_kmalloc_ex_( + (sizeof(struct kiss_hashent *)) * hsize, + "kiss_hash_create", + FW_KMEM_SLEEP, + __FILE__, + __LINE__ + ); + } else { + hp->h_tab = (struct kiss_hashent **)kiss_pmglob_memory_kmalloc( + (sizeof(struct kiss_hashent *)) * hsize, + "kiss_hash_create" + ); + } + + if (!hp->h_tab) { + kiss_pmglob_memory_kfree(hp, sizeof(struct kiss_hash), "kiss_hash_create"); + return NULL; + } + + memset(hp->h_tab, 0, (sizeof(struct kiss_hashent *) * hsize)); + + hp->h_sz = hsize - 1; + hp->h_orig_size = hp->h_sz; + hp->hash_index = -1; + hp->h_keyfunc = keyfunc == (hkeyfunc_t)kiss_hash_intvalue ? 0 : keyfunc; + hp->h_keycmp = keycmp == (hcmpfunc_t)kiss_hash_intcmp ? 0 : keycmp; + hp->h_val_destr = hp->h_key_destr = NULL; + hp->h_info = info; + hp->h_nelements = 0; + hp->h_dodestr = 0; + KissHashResizeMode_reset_parameters(&(hp->h_resize_mode)); + + return hp; +} + + +// Create Hash Table. +// +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hsize hash size +// @param keyfunc key hashing function +// @param keycmp key comparison function +// @param info// opaque for use of {\tt keyfunc} and {\tt keycmp} functions. +// @return hash pointer or NULL upon failure. +// @see kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, kiss_hash_delete, +// kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, +// kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp, kiss_hash_debug, kiss_hash_debug_all +// note: to create a large hash in kernel mode using kmalloc_sleep call function _kiss_hash_create_with_ksleep or +// Macro _kiss_hash_create_with_ksleep. +kiss_hash_t +kiss_hash_create(size_t hsize, + hkeyfunc_t keyfunc, + hcmpfunc_t keycmp, + void *info) +{ + return kiss_hash_create_do(hsize, keyfunc, keycmp, info, FALSE); +} + +kiss_hash_t +_kiss_hash_create(size_t hsize, + hkeyfunc_t keyfunc, + hcmpfunc_t keycmp, + void *info, CP_MAYBE_UNUSED const char *file, CP_MAYBE_UNUSED int line) +{ + kiss_hash_t hash; + hash = kiss_hash_create_do(hsize, keyfunc, keycmp, info, FALSE); + +#ifdef HASH_DEBUG + if (hash) dbg_register_hash(hash, line, file); +#endif + + return hash; +} + +kiss_hash_t +_kiss_hash_create_with_ksleep(size_t hsize, + hkeyfunc_t keyfunc, + hcmpfunc_t keycmp, + void *info, CP_MAYBE_UNUSED const char *file, CP_MAYBE_UNUSED int line) +{ + kiss_hash_t hash; + hash = kiss_hash_create_do(hsize, keyfunc, keycmp, info, TRUE); + +#ifdef HASH_DEBUG + if (hash) dbg_register_hash(hash, line, file); +#endif + + return hash; +} + + +// Set destructor for hash elements. +// +// Keys and values detsructors are called for every hash key-value pair +// when the hash is destroyed. +// +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hp hash +// @param val_destr destructor for the values of the hash +// @param key_destr destructor for the keys of the hash +// @return hash pointer +// @see kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, kiss_hash_delete, +// kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, +// kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp +kiss_hash_t +kiss_hash_set_destr(kiss_hash_t hp, freefunc_t val_destr, freefunc_t key_destr) +{ + if (!hp) return NULL; + + hp->h_val_destr = val_destr; + hp->h_key_destr = key_destr; + + return hp; +} + + +// This tells the hash to automaticly call destructors when an entry gets +// deleted from the hash. Usualy this is not the case ! +// +// Enable hash element detsruction. +// +// Hash is created with destruction of elements disabled by default. +// This functions enables destruction upon a call to \ref{kiss_hash_destroy}. + +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hp hash +// @see kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, kiss_hash_delete, +// kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, +// kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp +void +kiss_hash_dodestr(kiss_hash_t hp) +{ + hp->h_dodestr=1; +} + + +// What's done must (have a way to) be undone. +// +// +// Disable hash element detsruction. +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hp hash +// @see kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, kiss_hash_delete, +// kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, +// kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp +void +kiss_hash_undo_destr(kiss_hash_t hp) +{ + hp->h_dodestr = 0; +} + + +// Create Hash Table with Destructor. +// +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hsize hash size +// @param keyfunc key hashing function +// @param keycmp key comparison function +// @param val_destr destructor for the values of the hash +// @param key_destr destructor for the keys of the hash +// @param info// opaque for use of {\tt keyfunc} and {\tt keycmp} functions. +// @return hash pointer or NULL upon failure. +// @see kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, kiss_hash_delete, +// kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, +// kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp +kiss_hash_t +kiss_hash_create_with_destr( + size_t hsize, + hkeyfunc_t keyfunc, + hcmpfunc_t keycmp, + freefunc_t val_destr, + freefunc_t key_destr, + void *info +) +{ + kiss_hash_t hp; + + if ((hp = kiss_hash_create(hsize, keyfunc, keycmp, info)) == NULL) return NULL; + + return kiss_hash_set_destr(hp, val_destr, key_destr); +} + +kiss_hash_t +_kiss_hash_create_with_destr( + size_t hsize, + hkeyfunc_t keyfunc, + hcmpfunc_t keycmp, + freefunc_t val_destr, + freefunc_t key_destr, + void *info, CP_MAYBE_UNUSED const char *file, + CP_MAYBE_UNUSED int line) +{ + kiss_hash_t hash; + hash = kiss_hash_create_with_destr(hsize, keyfunc, keycmp, val_destr, key_destr, info); + +#ifdef HASH_DEBUG + if (hash) dbg_register_hash(hash, line, file); +#endif + + return hash; +} + + +// Find hash entry. +// +// The next routine is used as an efficient but somewhat ugly interface for +// find/insert operation. What it does is to return an adrress of a pointer +// to a hashent structure containing the key/val pair if found. If not it +// returns the address of the pointer in which we can append the new val/pair +// thus avoiding an unnceccessary repeated search. We can check if key was +// found by checking whether the pointer is zero or not. This function is usually +// used with \Ref{kiss_hash_insert_at}. +// +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hp hash pointer +// @param key hash key +// @return hash entry +// @see kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, kiss_hash_delete, +// kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, +// kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp +// +// @args (kiss_hash_t hp, const void *key) +// @type struct hashent ** +// @name kiss_hash_find_hashent. +struct kiss_hashent ** +kiss_hash_find_hashent(kiss_hash_t hp, const void *key) +{ + intptr_t slot = ((hp->h_keyfunc ? (*hp->h_keyfunc)(key, (hp)->h_info) : + ((intptr_t)key + ((intptr_t)key >> 16))) & (hp)->h_sz); + + struct kiss_hashent **pnt = hp->h_tab + slot; + struct kiss_hashent *he; + + if (hp->h_keycmp) { + for (he = *pnt; he != NULL; pnt = &(he->next), he = *pnt) { + if ((*hp->h_keycmp)(he->key, key, hp->h_info) == 0) return pnt; + } + } else { + for (he = *pnt; he != NULL; pnt = &(he->next), he = *pnt) { + if (he->key == key) return pnt; + } + } + + return pnt; +} + + +// Return address of the pointer to the value in the hash table. +// +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hp hash pointer +// @param key hash key +// @return hash entry +// @see kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, kiss_hash_delete, +// kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, +// kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp +void ** +kiss_hash_findaddr(kiss_hash_t hp, const void *key) +{ + struct kiss_hashent **he = kiss_hash_find_hashent(hp, key); + + if (!*he) return NULL; + + return &((*he)->val); +} + + +// Insert hash element at specified position. +// This function should be used together with \Ref{kiss_hash_find_hashent} to insert +// the value in case it was not found at the hash. +// +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hp hash pointer +// @param key hash key +// @param key hash val +// @return 0 - upon failure or number of hash elements after insertion in case of success. +// @see kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, kiss_hash_delete, +// kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, +// kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp +int +kiss_hash_insert_at(kiss_hash_t hp, void *key, void *val, struct kiss_hashent **hloc) +{ + struct kiss_hashent *he; + + he = (struct kiss_hashent *)kiss_pmglob_memory_kmalloc(sizeof(struct kiss_hashent), "kiss_hash_insert_at"); + + if (he == NULL) return 0; + + memset(he, 0, sizeof(struct kiss_hashent)); + + he->key = key; + he->val = val; + he->next = 0; + + *hloc = he; + hp->h_nelements++; + + if (kiss_hash_resize_check_for_resize(hp, KISS_HASH_SIZE_INCREASE) == TRUE) { + kiss_hash_do_resize(hp, &(hp->h_resize_mode)); + } + + return hp->h_nelements; +} + + +// Insert hash element. +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hp hash pointer +// @param key hash key +// @param key hash val +// @return 0 - upon failure, positive number on success. +// @see kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, kiss_hash_delete, +// kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, +// kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp +int +kiss_hash_insert(kiss_hash_t hp, void *key, void *val) +{ + struct kiss_hashent **hloc = kiss_hash_find_hashent(hp, key); + + if (*hloc) { + hent_destroy(hp, *hloc, 0); + (*hloc)->val = val; + (*hloc)->key = key; + return 1; + } + + return kiss_hash_insert_at(hp, key, val, hloc); +} + + +// Lookup hash value. +// +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hp hash pointer +// @param key hash key +// @return hash value or NULL upon failure. +// @see kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, kiss_hash_delete, +// kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, +// kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp +void * +kiss_hash_lookup(kiss_hash_t hp, const void *key) +{ + struct kiss_hashent **he = kiss_hash_find_hashent(hp, key); + + if (*he) return (*he)->val; + + return NULL; +} + + +// Lookup hash key. +// +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hp hash pointer +// @param key hash key that hash a value equal to that of the key stored in the hash. +// @return hash key or NULL upon failure. +// @see kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, kiss_hash_delete, +// kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, +// kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp +void * +kiss_hash_lookkey(kiss_hash_t hp, const void *key) +{ + struct kiss_hashent **he = kiss_hash_find_hashent(hp, key); + + if (*he) return (*he)->key; + + return NULL; +} + + +// Delete hash element. +// +// Delete hash element and return a value for the key. +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hp hash pointer +// @param key hash key +// @return hash val or NULL upon failure. +// @see kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, kiss_hash_delete, +// kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, +// kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp +void * +kiss_hash_delete(kiss_hash_t hp, const void *key) +{ + struct kiss_hashent **hloc = kiss_hash_find_hashent(hp, key); + struct kiss_hashent *he = *hloc; + + if (he) { + void *val = he->val; + *hloc = he->next; + hp->h_nelements--; + hent_destroy(hp, he, 0); + + kiss_pmglob_memory_kfree(he, sizeof(struct kiss_hashent), "kiss_hash_delete"); + + if (kiss_hash_resize_check_for_resize(hp, KISS_HASH_SIZE_DECREASE) == TRUE) + kiss_hash_do_resize(hp, &(hp->h_resize_mode)); + + return val; + } + + return NULL; +} + + +// Destroy hash. +// +// If detsructor functions were defined in the call to \Ref{kiss_hash_with_create_destr} or \Ref{kiss_hash_set_destr} +// function \Ref{kiss_hash_dodestr} must be called to enable element detsruction. +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hp hash pointer +// @see kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, kiss_hash_delete, +// kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, +// kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp +void +kiss_hash_destroy(kiss_hash_t hp) +{ + int i; + struct kiss_hashent *he, *np; + + for (i = 0; i <= hp->h_sz; i++) { + for (he = hp->h_tab[i]; he != NULL; he = np) { + np = he->next; + hent_destroy(hp, he, 1); + kiss_pmglob_memory_kfree(he, sizeof(struct kiss_hashent), "kiss_hash_destory"); + } + } + + if (hp->h_tab) { + kiss_pmglob_memory_kfree(hp->h_tab, (sizeof(struct kiss_hashent *) * (hp->h_sz+1)), "kiss_hash_destroy"); + } + +#ifdef HASH_DEBUG + dbg_deregister_hash(hp); +#endif + + kiss_pmglob_memory_kfree(hp, sizeof(struct kiss_hash), "kiss_hash_destroy"); + return; +} + + +// @name Hash iteration +// +// Create hash iterator. +// +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hp hash +// @return iterator object, or NULL upon failure. +// @see kiss_hash_iterator_create, kiss_hash_iterator_next, kiss_hash_iterator_next_key, kiss_hash_iterator_destroy +kiss_hash_iterator +kiss_hash_iterator_create(kiss_hash_t hp) +{ + kiss_hash_iterator hit = (kiss_hash_iterator)kiss_pmglob_memory_kmalloc( + sizeof (struct kiss_hash_iter), + "kiss_hash_iterator_create" + ); + + if (hit == NULL) return NULL; + + memset(hit, 0, sizeof (struct kiss_hash_iter)); + + hit->hash = hp; + hit->slot = 0; + hit->pntr = hit->hash->h_tab[0]; + + if (!hit->pntr) kiss_hash_iterator_next_ent(hit); + + return hit; +} + + +// Return next hash value. +// +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hit hash iterator +// @return next hash value, or NULL upon failure. +// @see kiss_hash_iterator_create, kiss_hash_iterator_next, kiss_hash_iterator_next_key, kiss_hash_iterator_destroy +void* +kiss_hash_iterator_next(kiss_hash_iterator hit) +{ + struct kiss_hashent *hent; + void *output; + + if (!(hent = hit->pntr)) { + int slot = hit->slot + 1; + struct kiss_hashent ** htab = hit->hash->h_tab; + int sz = hit->hash->h_sz; + + while (slot <= sz && ! (hent = htab[slot])) { + slot++; + } + + hit->slot = slot; + if (slot > sz) return NULL; + } + + output = hent->val; + hit->pntr = hent->next; + + return output; +} + + +// Return next hash key. +// +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hit hash iterator +// @return next hash key, or NULL upon failure. +// @see kiss_hash_iterator_create, kiss_hash_iterator_next, kiss_hash_iterator_next_key, kiss_hash_iterator_destroy +void* +kiss_hash_iterator_next_key(kiss_hash_iterator hit) +{ + struct kiss_hashent *hent; + void *output; + + if (!(hent = hit->pntr)) { + int slot = hit->slot + 1; + struct kiss_hashent ** htab = hit->hash->h_tab; + int sz = hit->hash->h_sz; + + while (slot <= sz && ! (hent=htab[slot])) + slot++; + + hit->slot = slot; + if (slot > sz) return NULL; + } + + output = hent->key; + hit->pntr = hent->next; + + return output; +} + + +// Destroy hash iterator. +// +// \begin{description} +// \item[ MT-Level: ] Reentrant +// \end{description} +// +// @param hit hash iterator +// @see kiss_hash_iterator_create, kiss_hash_iterator_next, kiss_hash_iterator_next_key, kiss_hash_iterator_destroy +void +kiss_hash_iterator_destroy (kiss_hash_iterator hit) +{ + kiss_pmglob_memory_kfree(hit, sizeof(struct kiss_hash_iter), "kiss_hash_iterator_destroy"); +} + + +int +kiss_hash_iterator_end(kiss_hash_iterator hit) +{ + return hit->slot == -1; +} + + +int +kiss_hash_iterator_next_ent(kiss_hash_iterator hit) +{ + struct kiss_hashent *hent; + + if (kiss_hash_iterator_end(hit)) return 0; + + if (! hit->pntr || ! hit->pntr->next) { + int slot = hit->slot + 1; + struct kiss_hashent ** htab = hit->hash->h_tab; + int sz = hit->hash->h_sz; + + while (slot <= sz && ! (hent=htab[slot])) { + slot++; + } + + if (slot > sz) { + kiss_hash_iterator_set_end(hit); + return 0; + } + else { + hit->slot = slot; + hit->pntr = hent; + } + } + else { + hit->pntr = hit->pntr->next; + } + + return 1; +} + + +void * +kiss_hash_iterator_get_key(kiss_hash_iterator hit) +{ + return hit->pntr ? hit->pntr->key : NULL; +} + + +void * +kiss_hash_iterator_get_val(kiss_hash_iterator hit) +{ + return hit->pntr ? hit->pntr->val : NULL; +} + + +struct kiss_hashent * +kiss_hash_iterator_get_hashent(kiss_hash_iterator hit) +{ + return hit->pntr; +} + +int +kiss_hash_iterator_equal(kiss_hash_iterator hit1, kiss_hash_iterator hit2) +{ + if (hit1->pntr || hit2->pntr) { + return hit1->pntr == hit2->pntr; + } + return hit1->slot == hit2->slot && hit1->hash == hit2->hash; +} + + +kiss_hash_iterator +kiss_hash_iterator_copy(kiss_hash_iterator hit) +{ + kiss_hash_iterator new_hit = (kiss_hash_iterator)kiss_pmglob_memory_kmalloc( + sizeof(struct kiss_hash_iter), + "kiss_hash_iterator_copy" + ); + if (hit == NULL || new_hit == NULL) return NULL; + + memset(new_hit, 0, sizeof (struct kiss_hash_iter)); + + new_hit->hash = hit->hash; + new_hit->slot = hit->slot; + new_hit->pntr = hit->pntr; + + return new_hit; +} + + +void +kiss_hash_iterator_free(kiss_hash_iterator hit) +{ + if (hit) kiss_pmglob_memory_kfree(hit, sizeof(struct kiss_hash_iter), "kiss_hash_iterator_free"); +} + +void +kiss_hash_iterator_set_begin(kiss_hash_iterator hit) +{ + hit->slot = 0; + hit->pntr = hit->hash->h_tab[0]; + + if (!hit->pntr) kiss_hash_iterator_next_ent(hit); +} + +void +kiss_hash_iterator_set_end(kiss_hash_iterator hit) +{ + hit->slot = -1; + hit->pntr = 0; +} + + +kiss_hash_iterator +kiss_hash_find_hashent_new(kiss_hash_t hp, const void *key) +{ + int slot = ((hp->h_keyfunc ? (*hp->h_keyfunc)(key, (hp)->h_info) : + ((intptr_t)key + ((intptr_t)key >> 16))) & (hp)->h_sz); + + struct kiss_hashent *pnt = hp->h_tab[slot]; + + kiss_hash_iterator iter; + + iter = kiss_hash_iterator_create(hp); + + if (hp->h_keycmp) { + for (; pnt != NULL; pnt = pnt->next) { + if ((*hp->h_keycmp)(pnt->key, key, hp->h_info) == 0) break; + } + } else { + for (; pnt != NULL; pnt = pnt->next) { + if (pnt->key == key) break; + } + } + + if (pnt == NULL) { + kiss_hash_iterator_set_end(iter); + } else { + iter->slot = slot; + iter->pntr = pnt; + } + + return iter; +} + + +void +kiss_hash_delete_by_iter(kiss_hash_iterator hit) +{ + if (hit == NULL || + kiss_hash_iterator_end(hit) || + kiss_hash_iterator_get_hashent(hit) == NULL) + return; + + kiss_hash_delete(hit->hash, kiss_hash_iterator_get_key(hit)); + + return; +} + +//= == === ==== ===== ====== ======= ======== +//= == === ==== ===== ====== ======= +// H a s h r e s i z e m e c h a n i s m +//= == === ==== ===== ====== ======= +//= == === ==== ===== ====== ======= ======== + + +// ----------------------------- +// KissHashResizeMode access API +// ----------------------------- +#ifdef KERNEL +#define herror // this is done due to compilation errors after the merge from Trini to Dal +#endif + +int +KissHashResizeMode_create(KissHashResizeMode **resize_mode) +{ + KissHashResizeMode *_resize_mode = NULL; + + if (!resize_mode) { + herror(0, 0, "KissKissHashResizeMode_create: NULL resize-mode pointer"); + return -1; + } + _resize_mode = (KissHashResizeMode *)kiss_pmglob_memory_kmalloc( + sizeof(KissHashResizeMode), + "KissHashResizeMode_create" + ); + + if (!_resize_mode) { + herror(0, 0, "KissHashResizeMode_create: Unable to allocate space for KissHashResizeMode object"); + return -1; + } + + memset(_resize_mode, 0, sizeof(KissHashResizeMode)); + + // Set default resize parameters + KissHashResizeMode_set_default_parameters(_resize_mode); + + *resize_mode = _resize_mode; + + return 0; +} + +void +KissHashResizeMode_destroy(KissHashResizeMode *resize_mode) +{ + if (!resize_mode) { + herror(0, 0, "KissHashResizeMode_destroy: NULL resize-mode pointer"); + return; + } + kiss_pmglob_memory_kfree(resize_mode, sizeof(KissHashResizeMode), "KissHashResizeMode_destroy"); + + return; +} + +int +KissHashResizeMode_set_method( + KissHashResizeMode *resize_mode, + KissHashResizeMethod method, + u_int value, + u_int trigger_ratio +) +{ + KissHashResizeMode _resize_mode; + int rc = 0; + + if (!resize_mode) { + herror(0, 0, "KissHashResizeMode_set_method: NULL resize-mode pointer"); + return -1; + } + + // set method + _resize_mode.method = method; + rc = KissHashResizeMode_verify_method(&_resize_mode); + if (rc < 0) return -1; + + // set value + _resize_mode.value = value; + if (KissHashResizeMode_verify_value(&_resize_mode) < 0) return -1; + + // set trigger ratio + _resize_mode.trigger_ratio = trigger_ratio; + if (KissHashResizeMode_verify_trigger_ratio(&_resize_mode) < 0) return -1; + + resize_mode->method = method; + resize_mode->value = value; + resize_mode->trigger_ratio = trigger_ratio; + + return 0; +} + +int +KissHashResizeMode_get_method( + const KissHashResizeMode *resize_mode, + KissHashResizeMethod *method, + u_int *value, + u_int *trigger_ratio +) +{ + if (!resize_mode || !method || !value || !trigger_ratio) { + herror( + 0, + 0, + "KissHashResizeMode_get_method: NULL parameter (mode=%p, method=%p, value=%p, trig=%p)", + resize_mode, + method, + value, + trigger_ratio + ); + return -1; + } + *method = resize_mode->method; + *value = resize_mode->value; + *trigger_ratio = resize_mode->trigger_ratio; + + return 0; +} + +int +KissHashResizeMode_set_direction(KissHashResizeMode *resize_mode, KissHashResizeDirection direction) +{ + if (!resize_mode) { + herror(0, 0, "KissHashResizeMode_set_direction: NULL resize-mode pointer"); + return -1; + } + resize_mode->direction = direction; + + if (KissHashResizeMode_verify_direction(resize_mode) < 0) { + resize_mode->direction = KISS_HASH_SIZE_INC_DEC; + return -1; + } + + return 0; +} + +int +KissHashResizeMode_get_direction(const KissHashResizeMode *resize_mode, KissHashResizeDirection *direction) +{ + if (!resize_mode || !direction) { + herror( + 0, + 0, + "KissHashResizeMode_get_direction: NULL parameter (mode=%p; direction=%p)", + resize_mode, + direction + ); + return -1; + } + *direction = resize_mode->direction; + + return 0; +} + +int +KissHashResizeMode_set_max_size(KissHashResizeMode *resize_mode, u_int max_size) +{ + if (!resize_mode) { + herror(0, 0, "KissHashResizeMode_set_max_size: NULL resize-mode pointer"); + return -1; + } + resize_mode->max_size = max_size; + + return 0; +} + +int +KissHashResizeMode_get_max_size(const KissHashResizeMode *resize_mode, u_int *max_size) +{ + if (!resize_mode || !max_size) { + herror(0, 0, "KissHashResizeMode_get_max_size: NULL parameter (mode=%p; max_size=%p)", resize_mode, max_size); + return -1; + } + *max_size = resize_mode->max_size; + + return 0; +} + +int +kiss_hash_set_resize_cb(kiss_hash_t hp, HashResizeCb_t resize_callback) +{ + if (!hp) { + herror(0, 0, "kiss_hash_set_resize_cb: NULL hash pointer"); + return -1; + } + hp->h_resize_mode.cb = resize_callback; + + return 0; +} + +static void +KissHashResizeMode_reset_parameters(KissHashResizeMode *resize_mode) +{ + resize_mode->max_size = DEFAULT_KISS_HASH_SIZE; + resize_mode->method = KISS_HASH_RESIZE_METHOD_UNKNOWN; + resize_mode->direction = KISS_HASH_SIZE_STATIC; + resize_mode->value = 0; + resize_mode->trigger_ratio = 0; + + return; +} + +static void +KissHashResizeMode_set_default_parameters(KissHashResizeMode *resize_mode) +{ + resize_mode->max_size = DEFAULT_KISS_HASH_SIZE; + resize_mode->method = KISS_HASH_RESIZE_BY_FACTOR; + resize_mode->direction = KISS_HASH_SIZE_INC_DEC; + resize_mode->value = DEFAULT_KISS_HASH_RESIZE_FACTOR_VALUE; + resize_mode->trigger_ratio = DEFAULT_KISS_HASH_RESIZE_FACTOR_TRIG_RATIO; + + return; +} + +// ------------------------------------------------------------------------ +// KissHashResizeMode parameters verification & default values function +// ------------------------------------------------------------------------ +// Min & max values for a single hash resize +#define HASH_RESIZE_MIN_FACTOR_VALUE 2 +#define HASH_RESIZE_MAX_FACTOR_VALUE 8 +#define HASH_RESIZE_MIN_TRIG_FACTOR 2 +#define HASH_RESIZE_MAX_TRIG_FACTOR 8 + + +static int +KissHashResizeMode_verify_method(const KissHashResizeMode *resize_mode) +{ + if (resize_mode->method != KISS_HASH_RESIZE_BY_FACTOR) { + herror(0, 0, "KissHashResizeMode_verify_method: Illegal resize method (%d)", resize_mode->method); + return -1; + } + + return 0; +} + +static int +KissHashResizeMode_verify_value(const KissHashResizeMode *resize_mode) +{ + if (resize_mode->value == 0) + return -1; + + if (resize_mode->method == KISS_HASH_RESIZE_BY_FACTOR) { + if ( (resize_mode->value < HASH_RESIZE_MIN_FACTOR_VALUE) || + (resize_mode->value > HASH_RESIZE_MAX_FACTOR_VALUE) ) { + herror( + 0, + 0, + "KissHashResizeMode_verify_value: Illegal factor value (%d) - should be %d..%d", + resize_mode->value, + HASH_RESIZE_MIN_FACTOR_VALUE, + HASH_RESIZE_MAX_FACTOR_VALUE + ); + return -1; + } + } else { + return -1; + } + + return 0; +} + +static int +KissHashResizeMode_verify_trigger_ratio(const KissHashResizeMode *resize_mode) +{ + if (resize_mode->method == KISS_HASH_RESIZE_BY_FACTOR) { + if ((resize_mode->trigger_ratio < HASH_RESIZE_MIN_TRIG_FACTOR) || + (resize_mode->trigger_ratio > HASH_RESIZE_MAX_TRIG_FACTOR)) { + herror( + 0, + 0, + "KissHashResizeMode_verify_trigger_ratio: Illegal trigger value (%d) - should be %d..%d", + resize_mode->trigger_ratio, + HASH_RESIZE_MIN_TRIG_FACTOR, + HASH_RESIZE_MAX_TRIG_FACTOR + ); + return -1; + } + } else { + return -1; + } + + return 0; +} + +static int +KissHashResizeMode_verify_direction(const KissHashResizeMode *resize_mode) +{ + if ((resize_mode->direction != KISS_HASH_SIZE_STATIC) && + (resize_mode->direction != KISS_HASH_SIZE_INCREASE) && + (resize_mode->direction != KISS_HASH_SIZE_DECREASE) && + (resize_mode->direction != KISS_HASH_SIZE_INC_DEC) ) { + herror(0, 0, "KissHashResizeMode_verify_direction: Illegal resize direction (%d)", resize_mode->direction); + return -1; + } + return 0; +} + +static int +KissHashResizeMode_verify_max_size(const kiss_hash_t hp, const KissHashResizeMode *resize_mode) +{ + if (kiss_hash_get_size(hp) > (int)resize_mode->max_size) { + herror( + 0, + 0, + "KissHashResizeMode_verify_max_size: Max size (%d) is lower than current hash size (%d)", + resize_mode->max_size, + kiss_hash_get_size(hp) + ); + return -1; + } + return 0; +} + +int +KissHashResizeMode_verify_params(const kiss_hash_t hp, const KissHashResizeMode *resize_mode) +{ + int rc = 0; + + if (!resize_mode) { + herror(0, 0, "KissHashResizeMode_verify_params: NULL resize-mode pointer"); + return -1; + } + + rc = KissHashResizeMode_verify_method(resize_mode); + if (rc==0) rc = KissHashResizeMode_verify_value(resize_mode); + if (rc==0) rc = KissHashResizeMode_verify_trigger_ratio(resize_mode); + if (rc==0) rc = KissHashResizeMode_verify_direction(resize_mode); + if (rc==0) rc = KissHashResizeMode_verify_max_size(hp, resize_mode); + + return rc; +} + +// ----------------------------------- +// Set hash to have dynamic size +// ----------------------------------- +int +kiss_hash_set_dynamic_size(kiss_hash_t hp, const KissHashResizeMode *resize_mode) +{ + if (!hp || !resize_mode) { + herror(0, 0, "kiss_hash_set_dynamic_size: NULL parameter (hp=%p; mode=%p)", hp, resize_mode); + return -1; + } + + if (KissHashResizeMode_verify_params(hp, resize_mode) < 0) { + herror(0, 0, "kiss_hash_set_dynamic_size: Illegal resize parameters"); + return -1; + } + + hp->h_resize_mode.max_size = resize_mode->max_size; + hp->h_resize_mode.method = resize_mode->method; + hp->h_resize_mode.direction = resize_mode->direction; + hp->h_resize_mode.value = resize_mode->value; + hp->h_resize_mode.trigger_ratio = resize_mode->trigger_ratio; + hp->h_resize_mode.cb = resize_mode->cb; + + return 0; +} + +int +kiss_hash_get_dynamic_size(kiss_hash_t hp, const KissHashResizeMode **resize_mode) +{ + if (!hp || !resize_mode) { + herror(0, 0, "kiss_hash_get_dynamic_size: NULL parameter (hp=%p; mode=%p)", hp, resize_mode); + return -1; + } + *resize_mode = &(hp->h_resize_mode); + + return 0; +} + +// -------------------------- +// "Manual" hash resizing +// -------------------------- +// +// This API will cause an immediate resizing of hash +// table, according to the parameters, given in the +// input KissHashResizeMode object. +// Note that the KissHashResizeMode object parameters are +// not kept on the hash handle for future resize oprations. +int +kiss_hash_trigger_resize(kiss_hash_t hp, const KissHashResizeMode *resize_mode) +{ + const KissHashResizeMode *mode = resize_mode ? resize_mode : &(hp->h_resize_mode); + + if (mode->direction == KISS_HASH_SIZE_STATIC) { + herror(0, 0, "kiss_hash_trigger_resize: Static resize mode"); + return -1; + } + + herror(0, 0, "kiss_hash_trigger_resize: Triggering hash resize"); + return kiss_hash_do_resize(hp, mode); +} + +// ----------------------- +// Resize hash table +// ----------------------- +// +// Check if resize should be triggered +static +boolean_cpt kiss_hash_resize_check_for_resize(kiss_hash_t hp, KissHashResizeDirection direction) +{ + if (!hp) return FALSE; + + // Static hash size remains fixed + if (hp->h_resize_mode.direction == KISS_HASH_SIZE_STATIC) return FALSE; + + // + // Size cannot change before number of elements + // is larger than original hash size. + if ((kiss_hash_get_size(hp) == kiss_hash_orig_size(hp)) && (kiss_hash_nelements(hp) < kiss_hash_orig_size(hp))) { + return FALSE; + } + + + // Do not expand hash with less elements than hash size. + // Do not shrink hash with more elements than hash size. + if (kiss_hash_nelements(hp) < kiss_hash_get_size(hp)) { + if ((hp->h_resize_mode.direction == KISS_HASH_SIZE_INCREASE) || (direction == KISS_HASH_SIZE_INCREASE)) { + return FALSE; + } + } + + if (kiss_hash_nelements(hp) > kiss_hash_get_size(hp)) { + if ((hp->h_resize_mode.direction == KISS_HASH_SIZE_DECREASE) || (direction == KISS_HASH_SIZE_DECREASE)) { + return FALSE; + } + } + + + if (hp->h_resize_mode.method == KISS_HASH_RESIZE_BY_FACTOR) { + if (kiss_hash_nelements(hp) >= (kiss_hash_get_size(hp) * (int)hp->h_resize_mode.trigger_ratio)) + return TRUE; + + if (kiss_hash_nelements(hp) <= (kiss_hash_get_size(hp) / (int)hp->h_resize_mode.value)) + return TRUE; + } + + return FALSE; +} + + +// Calculate a new hash size for hash resizing operation. +// +// Please note that new size is calculated differently upon +// increase & decrease operations (refer to design doc for +// more details). +static int +kiss_hash_resize_calc_new_size(const kiss_hash_t hp, const KissHashResizeMode *resize_mode) +{ + KissHashResizeDirection direction; + int h_new_size = -1; + + // Determine whether to increase or decrease hash size + if ((resize_mode->direction == KISS_HASH_SIZE_INCREASE) || (resize_mode->direction == KISS_HASH_SIZE_DECREASE)) { + direction = resize_mode->direction; + } else { + if (resize_mode->direction == KISS_HASH_SIZE_INC_DEC) { + if (kiss_hash_nelements(hp) >= kiss_hash_get_size(hp)) { + direction = KISS_HASH_SIZE_INCREASE; + } else { + direction = KISS_HASH_SIZE_DECREASE; + } + } else { + return -1; + } + } + + // Set new hash size + if (resize_mode->method == KISS_HASH_RESIZE_BY_FACTOR) { + if (direction == KISS_HASH_SIZE_INCREASE) { + h_new_size = kiss_hash_get_size(hp) * resize_mode->value; + } else { + h_new_size = kiss_hash_get_size(hp) / resize_mode->trigger_ratio; + } + } + else{ + return -1; + } + + // Hash sizes are rounded to the nearest power of 2. Same as in hash create + h_new_size = roundtwo(h_new_size); + + // Check that the new size does not break the allowed size limits + if (h_new_size > (int)resize_mode->max_size) { + herror( + 0, + 0, + "kiss_hash_resize_calc_new_size: New size (%d) exceeds the size limit (%d)", + h_new_size, + resize_mode->max_size + ); + return -1; + } + + // Hash size cannot decrease below its original value + if (h_new_size < kiss_hash_orig_size(hp)) { + herror( + 0, + 0, + "kiss_hash_resize_calc_new_size: New size (%d) is lower than the original size (%d)", + h_new_size, + kiss_hash_orig_size(hp) + ); + return -1; + } + + return h_new_size; +} + + +// Hash resize function. +// This function does the actual resize operation: +// 1. A temporary hash is created, with the new size +// 2. All elements from the original hash are inserted into the temp hash +// 3. Hash elements & size are switched between the orig & temp hash tables. +// 4. Temporary hash is destroyed. +// returns a negative value upon failure or new hash size on success. +#define EXIT_RESIZE(msg, rc) \ + if (temp_hash) { kiss_hash_destroy(temp_hash);} \ + if (orig_kiss_hash_iter) {kiss_hash_iterator_free(orig_kiss_hash_iter);} \ + if (msg != nullptr) {herror(0, 0, "kiss_hash_do_resize: %s", msg);} \ + return rc; + +static int +kiss_hash_do_resize(kiss_hash_t hp, const KissHashResizeMode *resize_mode) +{ + int orig_h_sz = 0, h_new_size = 0, rc = 0; + kiss_hash_t temp_hash = NULL; + struct kiss_hashent **orig_h_tab = NULL; + kiss_hash_iterator orig_kiss_hash_iter = NULL; + void *kiss_hash_key = NULL, *kiss_hash_val = NULL; + + if (!hp || !resize_mode) { + EXIT_RESIZE("NULL parameter", -1); + } + else + + if (KissHashResizeMode_verify_params(hp, resize_mode) < 0) { + EXIT_RESIZE("Illegal resize parameters", -1); + } + + // Calculate new hash size + h_new_size = kiss_hash_resize_calc_new_size(hp, resize_mode); + if (h_new_size <= 0) { + EXIT_RESIZE("Unable to set new hash size or hash cannot resize", -1); + } + + // Check that new & original hash tables do not have the same size + // (might happen due to the hash sizes being rounded to the nearest + // power of two, higher than the calculated size) + if (h_new_size == kiss_hash_get_size(hp)) { + EXIT_RESIZE("Original & new hash have the same size. No resize will be done.", -1); + } + + herror( + 0, + 0, + "kiss_hash_do_resize: Resizing hash from %d to %d (n_elements=%d)", + kiss_hash_get_size(hp), + h_new_size, kiss_hash_nelements(hp) + ); + + // Create a temporary hash table + temp_hash = kiss_hash_create(h_new_size, hp->h_keyfunc, hp->h_keycmp, hp->h_info); + if (!temp_hash) { + EXIT_RESIZE("Unable to allocate temporary hash", -1); + } + + // Move elements from original hash to temporary hash + orig_kiss_hash_iter = kiss_hash_iterator_create(hp); + if (!orig_kiss_hash_iter) { + EXIT_RESIZE("Failed to create hash iterator", -1); + } + + do { + if (!(kiss_hash_iterator_get_hashent(orig_kiss_hash_iter))) continue; + + kiss_hash_key = kiss_hash_iterator_get_key(orig_kiss_hash_iter); + kiss_hash_val = kiss_hash_iterator_get_val(orig_kiss_hash_iter); + rc = kiss_hash_insert(temp_hash, kiss_hash_key, kiss_hash_val); + if (!rc) { + herror(0, 0, "kiss_hash_do_resize: Failed to add to hash (key=%x; val=%x)", kiss_hash_key, kiss_hash_val); + EXIT_RESIZE("", -1); + } + } while(kiss_hash_iterator_next_ent(orig_kiss_hash_iter)); + + kiss_hash_iterator_free(orig_kiss_hash_iter); + orig_kiss_hash_iter = NULL; + + + // Replace original and temporary table-pointers and sizes + orig_h_tab = hp->h_tab; + orig_h_sz = hp->h_sz; + + hp->h_tab = temp_hash->h_tab; + hp->h_sz = temp_hash->h_sz; + + temp_hash->h_tab = orig_h_tab; + temp_hash->h_sz = orig_h_sz; + + // Destroy temporary hash. + // No application data is deleted since the temporary hash + // has no value or key destructors, and the h_dodestr flag + // is not set. + kiss_hash_destroy(temp_hash); + + // Notify application on hash resize + if (resize_mode->cb) resize_mode->cb(hp, hp->h_info); + + return kiss_hash_get_size(hp); +} +#undef EXIT_RESIZE + + +// Hashing fuction for string hash. +// This function is used by hash_strcreate(). +// @param vs key +// @param info opaque +// @return value of the hash function. +uintptr_t +kiss_hash_strvalue(const void *vs, CP_MAYBE_UNUSED void *info) +{ + unsigned int val; + const char* s = (const char *)vs; + + for(val = 0; *s; s++) { + val = ((val >> 3) ^ (val<<5)) + *s; + } + return val; +} + + +// Comparison fuction for string hash. +// This function is used by hash_strcreate(). +// +// @param vk1 key +// @param vk2 key +// @param info opaque +// @return 0 - keys are equal, otherwise different. +int +kiss_hash_strcmp(const void* vk1, const void* vk2, CP_MAYBE_UNUSED void *info) +{ + const char* k1 = (const char *)vk1; + const char* k2 = (const char *)vk2; + return strcmp(k1, k2); +} + + +// Hashing fuction for integer hash. +// This function is used by hash_intcreate(). +// @param v key +// @param info opaque +// @return value of the hash function. +uintptr_t +kiss_hash_intvalue(const void* v, CP_MAYBE_UNUSED void *info) +{ + return (uintptr_t)v; +} + + +// Comparison fuction for integer hash. +// This function is used by hash_intcreate(). +// +// @param vv1 key +// @param vv2 key +// @param info opaque +// @return 0 - keys are equal, otherwise different. +int +kiss_hash_intcmp(const void* vv1, const void* vv2, CP_MAYBE_UNUSED void *info) +{ + intptr_t v1 = (intptr_t)vv1; + intptr_t v2 = (intptr_t)vv2; + return v1 - v2; +} + + +#ifdef KERNEL +#undef herror +#endif +SASAL_END diff --git a/components/utils/pm/kiss_hash.h b/components/utils/pm/kiss_hash.h new file mode 100644 index 0000000..a424d68 --- /dev/null +++ b/components/utils/pm/kiss_hash.h @@ -0,0 +1,586 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __KISS_HASH_H__ +#define __KISS_HASH_H__ + +#include "general_adaptor.h" + +typedef struct kiss_hash *kiss_hash_t; + +struct kiss_hashent { + void *key; + void *val; + struct kiss_hashent *next; +}; + +typedef uintptr_t (*hkeyfunc_t)(const void *key, void *info); +typedef int (*hcmpfunc_t)(const void *key1, const void *key2, void *info); +typedef void (*freefunc_t)(void *info); + +// {group: API for KISS_HASH} +#define H_DESTR(destr, addr) \ +if (destr && (((uintptr_t)(addr)) > 0x10)) (*destr)(addr); + +// {group: API for KISS_HASH} +// Description: Create Hash Table. MT-Level: Reentrant +// Parameters: +// hsize - hash size +// keyfunc - key hashing function +// keycmp - key comparison function +// info - opaque for use of keyfunc and keycmp functions. +// Return values: +// o hash pointer +// o NULL upon failure +// See also: kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, +// kiss_hash_delete, kiss_hash_destroy, kiss_hash_find_kiss_hashent, kiss_hash_insert_at, kiss_hash_strvalue, +// kiss_hash_strcmp, kiss_hash_intvalue, kiss_hash_bytevalue, +// kiss_hash_bytecmp, kiss_hash_debug, kiss_hash_debug_all +kiss_hash_t kiss_hash_create (size_t hsize, hkeyfunc_t keyfunc, hcmpfunc_t keycmp, void *info); + +// {group: API for HASH} +// Description: Create Hash Table with Destructor. MT-Level: Reentrant +// Parameters: +// hsize - hash size +// keyfunc - key hashing function +// keycmp - key comparison function +// val_destr - destructor for the values of the hash +// key_destr - destructor for the keys of the hash +// info - opaque for use of keyfunc and keycmp functions. +// Return values: +// o hash pointer +// o NULL upon failure +// See also: kiss_hash_create, kiss_hash_set_destr, kiss_hash_dodestr, kiss_hash_undo_destr, kiss_hash_nelements, +// iss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, kiss_hash_delete, kiss_hash_destroy, +// kiss_hash_find_kiss_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, kiss_hash_intvalue, +// kiss_hash_bytevalue, kiss_hash_bytecmp, kiss_hash_debug, kiss_hash_debug_all +kiss_hash_t +kiss_hash_create_with_destr ( + size_t hsize, + hkeyfunc_t keyfunc, + hcmpfunc_t keycmp, + freefunc_t val_destr, + freefunc_t key_destr, + void *info +); + +#define kiss_hash_create(hsize, hkeyfunc, hcmpfunc, info) \ + _kiss_hash_create (hsize, hkeyfunc, hcmpfunc, info, __FILE__, __LINE__) + +#define kiss_hash_create_with_destr(hsize, hkeyfunc, hcmpfunc, freefunc1, freefunc2, info) \ + _kiss_hash_create_with_destr (hsize, hkeyfunc, hcmpfunc, freefunc1, freefunc2, info, __FILE__, __LINE__) + +kiss_hash_t +_kiss_hash_create_with_ksleep(size_t hsize, hkeyfunc_t, hcmpfunc_t, void *info, const char *file, int line); + +#define kiss_hash_create_with_ksleep(hsize, hkeyfunc, hcmpfunc, info) \ + _kiss_hash_create_with_ksleep (hsize, hkeyfunc, hcmpfunc, info, __FILE__, __LINE__) + + +// {group: API for HASH} +// Description: Debug single hash. MT-Level: Reentrant +//This function calculates and prints the following statistics: +//o hash pointer +//o file name and line number where kiss_hash_create or kiss_hash_create_with_destr was called +//o number of elements in kiss_hash +//o number of slots in hash - hash size +//o size in bytes of memory occupied by hash maintenance structures +//o slot utilzation - percentage of hash slots used to store elements +//o average number of lookups - average length of lists of elements +// Parameters: +// hash - pointer to hash +// Return values: +// size in bytes of memory occupied by hash maintenance structures. +// See also: hash_create, hash_create_with_destr, hash_set_destr, hash_dodestr, hash_undo_destr, +// hash_nelements, hash_findaddr, hash_lookup, hash_lookkey, hash_insert, hash_delete, hash_destroy, +// hash_find_hashent, hash_insert_at, hash_strvalue, hash_strcmp, hash_intvalue, hash_bytevalue, +// hash_bytecmp, hash_debug_all +int kiss_hash_debug(kiss_hash_t hp); + +// {group: API for HASH} +// Description: Debug single hash. MT-Level: Safe +//Iterates a list of all hash tables craeted in the current process and +//for each hash calls function hash_debug. In addition the total +//memory usage of hash maintenance structures is printed. +// See also: kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, +// kiss_hash_undo_destr, kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, +// kiss_hash_insert, kiss_hash_delete, kiss_hash_destroy, +// kiss_hash_find_kiss_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, kiss_hash_intvalue, +// kiss_hash_bytevalue, kiss_hash_bytecmp, kiss_hash_debug +void kiss_hash_debug_all(); + +// {group: API for kiss_hash} +kiss_hash_t _kiss_hash_create (size_t hsize, hkeyfunc_t, hcmpfunc_t, void *info, const char *file, int line); + +// {group: API for HASH} +kiss_hash_t _kiss_hash_create_with_destr (size_t hsize, hkeyfunc_t, hcmpfunc_t, freefunc_t, freefunc_t, + void *info, const char *file, int line); + +// {group: API for HASH} +// Description: Set destructor for hash elements. MT-Level: ] Reentrant +//Keys and values detsructors are called for every hash key-value pair when the hash is destroyed. +// Parameters: +// hp - hash +// val_destr - destructor for the values of the hash +// key_destr - destructor for the keys of the hash +// Return values: +// hash pointer +// See also: kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_dodestr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, +// kiss_hash_delete, kiss_hash_destroy, kiss_hash_find_kiss_hashent, kiss_hash_insert_at, kiss_hash_strvalue, +// kiss_hash_strcmp, kiss_hash_intvalue, kiss_hash_bytevalue, +// kiss_hash_bytecmp, kiss_hash_debug, kiss_hash_debug_all +kiss_hash_t kiss_hash_set_destr (kiss_hash_t hp, freefunc_t val_destr, freefunc_t key_destr); + +// {group: API for kiss_hash} +// Description: Enable hash element detsruction. MT-Level: Reentrant +//Hash is created with destruction of elements disabled by default. +//This function enables destruction upon a call to kiss_hash_destroy. +//Meaning, the hash will automaticly call destructors when an entry gets +//deleted from the hash. Usualy this is not the case ! +// Parameters: +// hp - hash +// See also: kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_undo_destr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, +// kiss_hash_delete, kiss_hash_destroy, kiss_hash_find_kiss_hashent, kiss_hash_insert_at, kiss_hash_strvalue, +// kiss_hash_strcmp, kiss_hash_intvalue, kiss_hash_bytevalue, +// kiss_hash_bytecmp, kiss_hash_debug, kiss_hash_debug_all +void kiss_hash_dodestr (kiss_hash_t hp); + +// {group: API for HASH} +// Description: Disable hash element detsruction. MT-Level: Reentrant +// Parameters: +// hp - hash +// See also: kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, +// kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, +// kiss_hash_delete, kiss_hash_destroy, +// kiss_hash_find_kiss_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, kiss_hash_intvalue, +// kiss_hash_bytevalue, kiss_hash_bytecmp, kiss_hash_debug, kiss_hash_debug_all +void kiss_hash_undo_destr (kiss_hash_t hp); + +// {group: API for HASH} +// Description: Number of hash elements. MT-Level: Reentrant +// Parameters: +// hash - hash table +// Return values: +// number of elements +// See also: kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, +// kiss_hash_undo_destr, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, +// kiss_hash_delete, kiss_hash_destroy, +// kiss_hash_find_kiss_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, kiss_hash_intvalue, +// kiss_hash_bytevalue, kiss_hash_bytecmp, kiss_kiss_hash_debug, kiss_hash_debug_all +int kiss_hash_nelements (kiss_hash_t hash); + +// {group: API for HASH} +// Description: Hash size. MT-Level: Reentrant +// Parameters: +// hash - hash table +// Return values: +// Size of hash +// See also: kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, +// kiss_hash_undo_destr, kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, +// kiss_hash_insert, kiss_hash_delete, kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, +// kiss_hash_strvalue, kiss_hash_strcmp, kiss_hash_intvalue, kiss_hash_bytevalue, +// kiss_hash_bytecmp, kiss_hash_debug, kiss_hash_debug_all +int kiss_hash_get_size (kiss_hash_t hash); + +// {group: API for HASH} +// Description: Return address of the pointer to the value in the hash table. +// Parameters: +// hp - hash pointer +// key - hash key +// Return values: +// hash entry +// See also: kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, +// kiss_hash_undo_destr, kiss_hash_nelements, kiss_hash_lookup, kiss_hash_lookkey, kiss_hash_insert, +// kiss_hash_delete, kiss_hash_destroy, +// kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, kiss_hash_intvalue, +// kiss_hash_bytevalue, kiss_hash_bytecmp, kiss_hash_debug, kiss_hash_debug_all +void **kiss_hash_findaddr (kiss_hash_t hp, const void *key); + +// {group: API for HASH} +// Description: Lookup hash value. MT-Level: Reentrant +// Parameters: +// hp - hash pointer +// key - hash key +// Return values: +// o hash value +// o NULL upon failure +// See also: kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, +// kiss_hash_undo_destr, kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookkey, kiss_hash_insert, +// kiss_hash_delete, kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, +// kiss_hash_strcmp, kiss_hash_intvalue, kiss_hash_bytevalue, +// kiss_hash_bytecmp, kiss_hash_debug, kiss_hash_debug_all +void *kiss_hash_lookup (kiss_hash_t hp, const void *key); + +// {group: API for HASH} +// Description: Lookup hash key. MT-Level: Reentrant +//Returns the key pointer as stored in the hash table. +// Parameters: +// hp - hash pointer +// key - hash key that hash a value equal to that of the key stored in the hash. +// Return values: +// o hash key +// o NULL upon failure +// See also: kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, +// kiss_hash_undo_destr, kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_insert, +// kiss_hash_delete, kiss_hash_destroy,kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, +// kiss_hash_strcmp, kiss_hash_intvalue, kiss_hash_bytevalue, +// kiss_hash_bytecmp, kiss_hash_debug, kiss_hash_debug_all +void *kiss_hash_lookkey (kiss_hash_t hp, const void *key); + +// {group: API for HASH} +// Description: Insert hash element. MT-Level: Reentrant +// Parameters: +// hp - hash pointer +// key - hash key +// val - hash val +// Return values: +// >0 - success +// 0 - upon failure +// See also: kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, +// kiss_hash_undo_destr, kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, +// kiss_hash_delete, kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, +// kiss_hash_strcmp, kiss_hash_intvalue, kiss_hash_bytevalue, +// kiss_hash_bytecmp, kiss_hash_debug, kiss_hash_debug_all +int kiss_hash_insert (kiss_hash_t hp, void *key, void *val); + +// {group: API for HASH} +// Description: Delete hash element. MT-Level: Reentrant +//Delete hash element and return a value for the key. +// Parameters: +// hp - hash pointer +// key - hash key +// Return values: +// o hash val +// o NULL upon failure +// See also: kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, +// kiss_hash_undo_destr, kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, +// kiss_hash_insert, kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, +// kiss_hash_strcmp, kiss_hash_intvalue, kiss_hash_bytevalue, +// kiss_hash_bytecmp, kiss_hash_debug, kiss_hash_debug_all +void *kiss_hash_delete (kiss_hash_t hash, const void *key); + +// {group: API for HASH} +// Description: Destroy hash. MT-Level: Reentrant +//If detsructor functions were defined in the call to kiss_hash_with_create_destr or kiss_hash_set_destr +//function kiss_hash_dodestr must be called to enable element detsruction. +// Parameters: +// hp - hash pointer +// See also: kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, +// kiss_hash_undo_destr,kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, +// kiss_hash_insert, kiss_hash_delete, kiss_hash_find_hashent, kiss_hash_insert_at, kiss_hash_strvalue, +// kiss_hash_strcmp, kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp, kiss_hash_debug, +// kiss_hash_debug_all +void kiss_hash_destroy (kiss_hash_t hp); + +// {group: API for HASH} +// Description: Find hash entry. MT-Level: Reentrant +//Used as an efficient but somewhat ugly interface for find/insert operation. +//What it does is to return an adrress of a pointer to a hashent structure containing the key/val pair if found. +//If not it returns the address of the pointer in which we can append the new val/pair +//thus avoiding an unnceccessary repeated search. +//We can check if key was found by checking whether the pointer is zero or not. +//This function is usually used with kiss_hash_insert_at. +// Parameters: +// hp - hash pointer +// key - hash key +// Return values: +// hash entry +// See also: kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, +// kiss_hash_undo_destr, kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, +// kiss_hash_insert, kiss_hash_delete, kiss_hash_destroy, kiss_hash_insert_at, kiss_hash_strvalue, kiss_hash_strcmp, +// kiss_hash_intvalue, kiss_hash_bytevalue, kiss_hash_bytecmp, kiss_hash_debug, kiss_hash_debug_all +struct kiss_hashent ** kiss_hash_find_hashent(kiss_hash_t hp, const void *key); + +// {group: API for HASH} +// Description: Insert hash element at specified position. MT-Level: Reentrant +//This function should be used together with kiss_hash_find_hashent to insert +//the value in case it was not found at the hash. +// Parameters: +// hp - hash pointer +// key - hash key +// val - hash val +// hloc - +// Return values: +// o 0 upon failure +// o number of hash elements after insertion in case of success. +// See also: kiss_hash_create, kiss_hash_create_with_destr, kiss_hash_set_destr, kiss_hash_dodestr, +// kiss_hash_undo_destr, kiss_hash_nelements, kiss_hash_findaddr, kiss_hash_lookup, kiss_hash_lookkey, +// kiss_hash_insert, kiss_hash_delete, +// kiss_hash_destroy, kiss_hash_find_hashent, kiss_hash_strvalue, kiss_hash_strcmp, kiss_hash_intvalue, +// kiss_hash_bytevalue, kiss_hash_bytecmp, kiss_hash_debug, kiss_hash_debug_all +int kiss_hash_insert_at (kiss_hash_t hp, void *key, void *val, struct kiss_hashent**hloc); + + +#define kiss_hash_strcreate(sz) \ + kiss_hash_create(sz, (hkeyfunc_t)kiss_hash_strvalue, (hcmpfunc_t)kiss_hash_strcmp, NULL) + +#define kiss_hash_intcreate(sz) \ + kiss_hash_create(sz, (hkeyfunc_t)kiss_hash_intvalue, (hcmpfunc_t)kiss_hash_intcmp, NULL) + +#define kiss_hash_bytecreate(n, esz) \ + kiss_hash_create(n, (hkeyfunc_t)kiss_hash_bytevalue, (hcmpfunc_t)kiss_hash_bytecmp, (void *)esz) + +// The following provide hash table data type interface, +// These functions can be provided by the user, +// The default provided functions provide string hash + +// {group: API for HASH} +// Description: Hashing fuction for string hash. +//This function is used by kiss_hash_strcreate(). +// Parameters: +// vs - key +// info - opaque +// Return values: +// value of the hash function. +uintptr_t kiss_hash_strvalue (const void *vs, void *info); + +// {group: API for HASH} +// Description: Comparison fuction for string hash. +//This function is used by kiss_hash_strcreate(). +// Parameters: +// vk1 - key +// vk2 - key +// info - opaque +// Return values: +// 0 - keys are equal +// !0 - keys are different +int kiss_hash_strcmp (const void *vk1, const void *vk2, void *info); + +// {group: API for HASH} +// Description: Hashing fuction for integer hash. +//This function is used by kiss_hash_intcreate(). +// Parameters: +// v - key +// info - opaque +// Return values: +// value of the hash function. +uintptr_t kiss_hash_intvalue (const void* v, void *info); + +// {group: API for HASH} +// Description: Comparison fuction for integer hash. +//This function is used by kiss_hash_intcreate(). +// Parameters: +// vv1 - key +// vv2 - key +// info - opaque +// Return values: +// 0 - keys are equal +// !0 - keys are different +int kiss_hash_intcmp (const void* vv1, const void* vv2, void *info); + +// {group: API for HASH} +// Description: Hashing fuction for byte hash. +//This function is used by kiss_hash_bytecreate(). +// Parameters: +// data - key +// info - opaque +// Return values: +// value of the hash function. +uintptr_t kiss_hash_bytevalue (const void *data, void *info); + +// {group: API for HASH} +// Description: Comparison fuction for byte hash. +//This function is used by kiss_hash_bytecreate(). +// Parameters: +// d1 - key +// d2 - key +// info - opaque +// Return values: +// 0 - keys are equal +// !0 - keys are different +int kiss_hash_bytecmp (const void *d1, const void *d2, void *info); + +// {group: API for HASH ITERATOR} +typedef struct kiss_hash_iter *kiss_hash_iterator; + +// {group: API for HASH ITERATOR} +// Description: Create hash iterator. MT-Level: Reentrant +// Parameters: +// hp - hash +// Return values: +// o iterator object +// o NULL upon failure +// See also: +// kiss_hash_iterator_next, kiss_hash_iterator_next_key, kiss_hash_iterator_destroy +kiss_hash_iterator kiss_hash_iterator_create (kiss_hash_t hp); + +// {group: API for HASH ITERATOR} +// Description: Return next hash value. MT-Level: Reentrant +// Parameters: +// hit - hash iterator +// Return values: +// o next hash value +// o NULL upon failure +// See also: +// kiss_hash_iterator_create, kiss_hash_iterator_next_key, kiss_hash_iterator_destroy +void *kiss_hash_iterator_next (kiss_hash_iterator hit); + +// {group: API for HASH ITERATOR} +// Description: Return next hash key. MT-Level: Reentrant +// Parameters: +// hit - hash iterator +// Return values: +// o next hash key +// o NULL upon failure +// See also: +// kiss_hash_iterator_create, kiss_hash_iterator_next, kiss_hash_iterator_destroy +void *kiss_hash_iterator_next_key (kiss_hash_iterator hit); + +// {group: API for HASH ITERATOR} +// Description: Destroy hash iterator. MT-Level: Reentrant +// Parameters: +// hit - hash iterator +// See also: +// kiss_hash_iterator_create, kiss_hash_iterator_next, kiss_hash_iterator_next_key +void kiss_hash_iterator_destroy (kiss_hash_iterator hit); + +// {group: API for ITERATOR} +int kiss_hash_iterator_next_ent(kiss_hash_iterator hit); + +// {group: API for ITERATOR} +void * kiss_hash_iterator_get_key(kiss_hash_iterator hit); + +// {group: API for ITERATOR} +void * kiss_hash_iterator_get_val(kiss_hash_iterator hit); + +// {group: API for ITERATOR} +struct kiss_hashent * kiss_hash_iterator_get_hashent(kiss_hash_iterator hit); + +// {group: API for ITERATOR} +int kiss_hash_iterator_equal(kiss_hash_iterator hit1, kiss_hash_iterator hit2); + +// {group: API for ITERATOR} +kiss_hash_iterator kiss_hash_iterator_copy(kiss_hash_iterator hit); + +// {group: API for ITERATOR} +void kiss_hash_iterator_free(kiss_hash_iterator hit); + +// {group: API for ITERATOR} +void kiss_hash_iterator_set_begin(kiss_hash_iterator hit); + +// {group: API for ITERATOR} +void kiss_hash_iterator_set_end(kiss_hash_iterator hit); + +// {group: API for HASH} +kiss_hash_iterator kiss_hash_find_hashent_new(kiss_hash_t hp, const void *key); + +// {group: API for HASH ITERATOR} +void kiss_hash_delete_by_iter(kiss_hash_iterator hit); + +// - - - - - - - - - - - - - - - +// Hash resize mechanism +// - - - - - - - - - - - - - - - + +// {group: API for HASH RESIZE} +// Determine if hash size can increase, decrease or both. +typedef enum { + KISS_HASH_SIZE_STATIC = 0, // hash size is kept fixed + KISS_HASH_SIZE_INCREASE = 1, + KISS_HASH_SIZE_DECREASE = 2, + KISS_HASH_SIZE_INC_DEC = 3 +} KissHashResizeDirection; + +// {group: API for HASH RESIZE} +typedef enum { + KISS_HASH_RESIZE_METHOD_UNKNOWN = 0, + KISS_HASH_RESIZE_BY_FACTOR = 1 +} KissHashResizeMethod; + +// {group: API for HASH RESIZE} +// Default maximal hash size: +// Hash size will not increase beyond this value unless stated o/w by the application +#define DEFAULT_KISS_HASH_SIZE (1<<17) + +// {group: API for HASH RESIZE} +// Default value for hash factorial resizing +#define DEFAULT_KISS_HASH_RESIZE_FACTOR_VALUE 4 +// {group: API for HASH RESIZE} +// Default value for hash factorial resizing trigger ratio +#define DEFAULT_KISS_HASH_RESIZE_FACTOR_TRIG_RATIO 2 + +// {group: API for HASH RESIZE} +// Resize application callback: This callback will be invoked at every successful resize operation. +typedef int (* HashResizeCb_t) (kiss_hash_t hp, void *app_info); + + +// Hash resize mode object & accsess API. +// Used for setting resize parameters hash. + +// {group: API for HASH RESIZE} +typedef struct _KissHashResizeMode KissHashResizeMode; + +// {group: API for HASH RESIZE} +int KissHashResizeMode_create(KissHashResizeMode **resize_mode); + +// {group: API for HASH RESIZE} +void KissHashResizeMode_destroy(KissHashResizeMode *resize_mode); + +// {group: API for HASH RESIZE} +int KissHashResizeMode_set_method( + KissHashResizeMode *resize_mode, + KissHashResizeMethod method, + u_int value, + u_int trigger_ratio); + +// {group: API for HASH RESIZE} +int KissHashResizeMode_get_method( + const KissHashResizeMode *resize_mode, + KissHashResizeMethod *method, + u_int *value, + u_int *trigger_ratio); + +// {group: API for HASH RESIZE} +int KissHashResizeMode_set_direction(KissHashResizeMode *resize_mode, KissHashResizeDirection direction); + +// {group: API for HASH RESIZE} +int KissHashResizeMode_get_direction(const KissHashResizeMode *resize_mode, KissHashResizeDirection *direction); + +// {group: API for HASH RESIZE} +int KissHashResizeMode_set_max_size(KissHashResizeMode *resize_mode, u_int max_size); + +// {group: API for HASH RESIZE} +int KissHashResizeMode_get_max_size(const KissHashResizeMode *resize_mode, u_int *max_size); + +// {group: API for HASH RESIZE} +int kiss_hash_set_resize_cb(kiss_hash_t hp, HashResizeCb_t resize_callback); + +// {group: API for HASH RESIZE} +// Description: Set hash dynamic size parameters. +// Parameters: +// hp - [in] pointer to hash table +// resize_mode - [in] should be created and set using the access API to the KissHashResizeMode object. +// After using the set API, this object can be destroyed. +// +int kiss_hash_set_dynamic_size(kiss_hash_t hp, const KissHashResizeMode *resize_mode); + +// {group: API for HASH RESIZE} +// Description: Get hash dynamic size parameters. +// Parameters: +// hp - [in] pointer to hash table +// resize_mode - [out] a read-only parameter that should not be changed by the application. +int kiss_hash_get_dynamic_size(kiss_hash_t hp, const KissHashResizeMode **resize_mode); + +// {group: API for HASH RESIZE} +// Description: This API will cause an immediate resizing of hash +// table, according to the parameters, given in the input +// KissHashResizeMode object (if NULL, the resize will be done +// according to the parameters as last set by the application). +// +// Note that the KissHashResizeMode object parameters are +// not kept on the hash handle for future resize oprations. +int kiss_hash_trigger_resize(kiss_hash_t hp, const KissHashResizeMode *resize_mode); + +#endif // __KISS_HASH_H__ diff --git a/components/utils/pm/kiss_patterns.cc b/components/utils/pm/kiss_patterns.cc new file mode 100644 index 0000000..17abfb7 --- /dev/null +++ b/components/utils/pm/kiss_patterns.cc @@ -0,0 +1,134 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "kiss_patterns.h" +#include +#include +#include "general_adaptor.h" +#include "pm_adaptor.h" +#include "sasal.h" + +SASAL_START // Multiple Pattern Matcher +// Add a character's printable representation to a buffer. +// Returns the number of bytes written. +static u_int +pm_str_one_char_to_debug_buf(u_char *buf, int len, u_char ch, BOOL for_csv) +{ + char single_char_buf[10]; + int single_char_len; + + // Get a printable representation of the character + if (isprint(ch) && !(ch == '"' && for_csv)) { + single_char_buf[0] = ch; + single_char_len = 1; + } else { + snprintf(single_char_buf, sizeof(single_char_buf), "\\x%02x", ch); + single_char_buf[sizeof(single_char_buf)-1] = '\0'; + single_char_len = strlen(single_char_buf); + } + + if (single_char_len > len) { + // See that we don't exceed the buffer, and leave room for \0. + single_char_len = len; + } + + bcopy(single_char_buf, buf, single_char_len); + return single_char_len; +} + +// Debug only - Returns a printable character pointer for the non null-terminated string +static const u_char * +pm_str_to_debug_charp_ex(const u_char *str, u_int size, BOOL for_csv) +{ + static u_char buf[200]; + u_int i; + u_char *buf_p; + + // Copy the string. But replace unprintable characters (most importantly \0) with underscores. + buf_p = &buf[0]; + for (i=0; i(buffer), size, _pattern_id, _flags) +{ +} + +kiss_pmglob_string_s::kiss_pmglob_string_s(const u_char *buffer, size_t size, int _pattern_id, u_int _flags) +{ + dbgAssert(buffer && size > 0) << "Illegal arguments"; + buf.resize(size); + memcpy(buf.data(), buffer, size); + pattern_id = _pattern_id; + flags = _flags; + return; +} + + +// Returns the pattern of the pattern as u_char* +int +kiss_pmglob_string_get_id(const kiss_pmglob_string_s *pm_string) +{ + KISS_ASSERT(pm_string != nullptr, "Illegal arguments"); + return pm_string->pattern_id; +} + + +// Returns the size of the pattern +u_int +kiss_pmglob_string_get_size(const kiss_pmglob_string_s * pm_string) +{ + KISS_ASSERT(pm_string != nullptr, "Illegal arguments"); + return pm_string->buf.size(); +} + +// Returns the pattern of the pattern as u_char* +const u_char * +kiss_pmglob_string_get_pattern(const kiss_pmglob_string_s *pm_string) +{ + KISS_ASSERT(pm_string != nullptr, "Illegal arguments"); + return pm_string->buf.data(); +} + + +// Debug only - Returns a printable character pointer for the string +const u_char * +kiss_pmglob_string_to_debug_charp(const kiss_pmglob_string_s *pm_string) +{ + return pm_str_to_debug_charp(kiss_pmglob_string_get_pattern(pm_string), kiss_pmglob_string_get_size(pm_string)); +} + + +u_int +kiss_pmglob_string_get_flags(const kiss_pmglob_string_s *pm_string) +{ + KISS_ASSERT(pm_string != nullptr, "Illegal arguments"); + return pm_string->flags; +} +SASAL_END diff --git a/components/utils/pm/kiss_patterns.h b/components/utils/pm/kiss_patterns.h new file mode 100644 index 0000000..523594b --- /dev/null +++ b/components/utils/pm/kiss_patterns.h @@ -0,0 +1,74 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __kiss_patterns_h__ +#define __kiss_patterns_h__ + +#include +#include +#include "pm_adaptor.h" + +// kiss_pmglob_string functions + +class kiss_pmglob_string_s { + public: + explicit kiss_pmglob_string_s(const char *buffer, size_t size, int _pattern_id, u_int _flags); + explicit kiss_pmglob_string_s(const u_char *buffer, size_t size, int _pattern_id, u_int _flags); + + std::vector buf; + int pattern_id; + u_int flags; +}; + + +// Returns the size of pattern +// +// Parameters: +// pattern - the pattern. +// Return value: +// int - the size that this pattern represents. +KISS_APPS_CPAPI +u_int kiss_pmglob_string_get_size(const kiss_pmglob_string_s *pattern); + +// Returns the pattern of the pattern as u_char* +// +// Parameters: +// patterns - the pattern. +// Return value: +// u_char * - the pattern that this pattern represents. +KISS_APPS_CPAPI +const u_char *kiss_pmglob_string_get_pattern(const kiss_pmglob_string_s *pattern); + +// For debugging only - returns a printable pointer for the string. +// Replaces unprintable characters with underscores. +// +// Note: In multithreaded situations, the buffer returned may be overrun by another thread. +// At worst, this would lead to an incorrect string being printed. +KISS_APPS_CPAPI +const u_char *kiss_pmglob_string_to_debug_charp(const kiss_pmglob_string_s *pm_string); + +// Returns the id of pattern +// +// Parameters: +// patterns - the pattern. +// Return value: +// id - the pattern_id that this pattern represents. +KISS_APPS_CPAPI +int kiss_pmglob_string_get_id(const kiss_pmglob_string_s *pattern); + + +KISS_APPS_CPAPI +u_int kiss_pmglob_string_get_flags(const kiss_pmglob_string_s *pattern); + + +#endif // __kiss_patterns_h__ diff --git a/components/utils/pm/kiss_pm_stats.cc b/components/utils/pm/kiss_pm_stats.cc new file mode 100644 index 0000000..62d7e0e --- /dev/null +++ b/components/utils/pm/kiss_pm_stats.cc @@ -0,0 +1,429 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "general_adaptor.h" +#include "sasal.h" +// ********************* INCLUDES ************************** +#include "kiss_pm_stats.h" +// ********************* INCLUDES ************************** + +SASAL_START // Multiple Pattern Matcher +// ********************* FUNCTIONS ************************** + + +// Initialize the common statistics +kiss_ret_val +kiss_pm_stats_common_init(kiss_pm_stats_common new_stats) +{ + static const char rname[] = "kiss_pm_stats_common_init"; + + if (new_stats == NULL) { + kiss_debug_err(K_PM, ("%s: stats is zero\n", rname)); + return KISS_ERROR; + } + + bzero(new_stats, sizeof(struct kiss_pm_stats_common_s)); + +#if 0 + if (kiss_pm_stats_take_exec_time) { + new_stats->exec_num_cpus = kiss_multik_instance_num; + new_stats->exec = kiss_pmglob_memory_kmalloc_ex( + new_stats->exec_num_cpus * sizeof(struct kiss_pm_stats_dynamic_aligned_s), + rname, + (FW_KMEM_NOSLEEP| FW_KMEM_RETURN_ALIGN_PTR) + ); + + if (!new_stats->exec) { + kiss_debug_err(K_PM, ("%s: Error in allocating the execution stats\n", rname)); + return KISS_ERROR; + } + + bzero(new_stats->exec, new_stats->exec_num_cpus*sizeof(struct kiss_pm_stats_dynamic_aligned_s)); + } +#endif + + return KISS_OK; + +} + +#define KISS_MULTIK_MAX_INSTANCE_NUM 40 + +// Free the common statistics +void +kiss_pm_stats_common_free(kiss_pm_stats_common stats) +{ + static const char rname[] = "kiss_pm_stats_common_free"; + BOOL should_free_stats_exec = + stats && + stats->exec && + stats->exec_num_cpus > 0 && + stats->exec_num_cpus < KISS_MULTIK_MAX_INSTANCE_NUM; + if (should_free_stats_exec) { + kiss_pmglob_memory_kfree( + stats->exec, + stats->exec_num_cpus * sizeof(struct kiss_pm_stats_dynamic_aligned_s), + rname + ); + stats->exec = NULL; + } + return; +} + +// Update build-time statistics +void +kiss_pm_stats_common_update_compile(kiss_pm_stats_common stats, u_int bytes, u_int compilation_time, + enum kiss_pm_stats_update_compile_type type) +{ + KISS_ASSERT_PERF(stats, ("Illegal arguments")); + + switch (type) { + case UPDATE_COMPILE_STATS_MEM: + stats->compile.memory_bytes = bytes; + return; + case UPDATE_COMPILE_STATS_TIME: + stats->compile.compilation_time = compilation_time; + return; + case UPDATE_COMPILE_STATS_BOTH: + stats->compile.memory_bytes = bytes; + stats->compile.compilation_time = compilation_time; + return; + } +} + + +// Will adding to an unsigned variable cause it to wrap around? +#define ADDITION_WOULD_WRAP_AROUND(old_val, delta) \ + ((old_val) + (delta) < (old_val)) + +// Reset buffer length statistics, so we can add a buffer without wraparound +static void +handle_buflen_stats_wraparound(struct kiss_pm_stats_dynamic_s *cur_kern_inst_stats) +{ + cur_kern_inst_stats->buflen.total = 0; + cur_kern_inst_stats->buflen.sample_num = 0; +} + +// Reset execution time statistics, so we can add a sample without wraparound +static void +handle_runtime_stats_wraparound(struct kiss_pm_stats_dynamic_s *cur_kern_inst_stats) +{ + cur_kern_inst_stats->runtime.total_exec_time = 0; + cur_kern_inst_stats->runtime.user_cb_exec_time = 0; + cur_kern_inst_stats->runtime.sample_num = 0; +} + + +// Update run-time statistics +void +kiss_pm_stats_common_update_exec(kiss_pm_stats_common stats, u_int buf_size, u_int num_of_matches) +{ + struct kiss_pm_stats_dynamic_s *cur_kern_inst_stats; + KISS_ASSERT_PERF(stats, ("Illegal arguments")); + if(stats->exec) { + ASSERT_LOCKED; + cur_kern_inst_stats = &(stats->exec[kiss_multik_this_instance_num].stats); + + // Buffer length statistics + if (ADDITION_WOULD_WRAP_AROUND(cur_kern_inst_stats->buflen.total, buf_size)) { + handle_buflen_stats_wraparound(cur_kern_inst_stats); + } + cur_kern_inst_stats->buflen.total += buf_size; + cur_kern_inst_stats->buflen.sample_num++; + if (buf_size > cur_kern_inst_stats->buflen.max) { + cur_kern_inst_stats->buflen.max = buf_size; + } + + // General statistics + cur_kern_inst_stats->num_of_buffs++; + cur_kern_inst_stats->num_of_matches += num_of_matches; + if (num_of_matches > cur_kern_inst_stats->max_matches_on_buf) { + cur_kern_inst_stats->max_matches_on_buf = num_of_matches; + } + } + + return; +} + +// Update run-time (execution) statistics +void +kiss_pm_stats_common_update_exec_time(kiss_pm_stats_common stats, u_int exec_time, u_int user_cb_time) +{ + struct kiss_pm_stats_dynamic_s *cur_kern_inst_stats; + if(stats && stats->exec) { + ASSERT_LOCKED; + cur_kern_inst_stats = &(stats->exec[kiss_multik_this_instance_num].stats); + + // The execution time includes the callback, but we want the net time. + exec_time -= user_cb_time; + + // take care of wrap around + if (ADDITION_WOULD_WRAP_AROUND(cur_kern_inst_stats->runtime.total_exec_time, exec_time) || + ADDITION_WOULD_WRAP_AROUND(cur_kern_inst_stats->runtime.user_cb_exec_time, user_cb_time)) { + handle_runtime_stats_wraparound(cur_kern_inst_stats); + } + cur_kern_inst_stats->runtime.total_exec_time += exec_time; + cur_kern_inst_stats->runtime.user_cb_exec_time += user_cb_time; + cur_kern_inst_stats->runtime.sample_num++; + + // Updating the max values + if (exec_time > cur_kern_inst_stats->runtime.max_exec_time){ + cur_kern_inst_stats->runtime.max_exec_time = exec_time; + } + if (user_cb_time > cur_kern_inst_stats->runtime.user_cb_max_time){ + cur_kern_inst_stats->runtime.user_cb_max_time = user_cb_time; + } + } + return; +} + + +// Clear all runtime statistics +void +kiss_pm_stats_common_reset_exec(kiss_pm_stats_common stats) +{ + u_int i; + if(stats && stats->exec) { + for (i = 0; i < stats->exec_num_cpus; i++) { + struct kiss_pm_stats_dynamic_s *cur_cpu_stats; + cur_cpu_stats = &(stats->exec[i].stats); + bzero(cur_cpu_stats, sizeof(*cur_cpu_stats)); + } + } +} + + +// Aggregate the run-time statistics from all cpus in src to dst +static void +kiss_pm_stats_common_aggregate_cpus(struct kiss_pm_stats_dynamic_s *dst, const struct kiss_pm_stats_common_s *src) +{ + u_int i; + KISS_ASSERT_PERF(src, ("Illegal arguments")); + if(src && src->exec) + { + for (i = 0; i < src->exec_num_cpus; i++) { + struct kiss_pm_stats_dynamic_s *cur_cpu_src = &(src->exec[i].stats); + + // Buffer length statistics - add and avoid wrap-around + if (ADDITION_WOULD_WRAP_AROUND(dst->buflen.total, cur_cpu_src->buflen.total)) { + handle_buflen_stats_wraparound(dst); + } + dst->buflen.total += cur_cpu_src->buflen.total; + dst->buflen.sample_num += cur_cpu_src->buflen.sample_num; + dst->buflen.max = MAX(dst->buflen.max, cur_cpu_src->buflen.max); + + // General statistics + dst->num_of_matches += cur_cpu_src->num_of_matches; + dst->num_of_stage1_matches += cur_cpu_src->num_of_stage1_matches; + dst->num_of_stage22_matches += cur_cpu_src->num_of_stage22_matches; + dst->num_of_stage23_matches += cur_cpu_src->num_of_stage23_matches; + + dst->num_of_buffs += cur_cpu_src->num_of_buffs; + if (dst->max_matches_on_buf < cur_cpu_src->max_matches_on_buf) { + dst->max_matches_on_buf = cur_cpu_src->max_matches_on_buf; + } + + // Execution time statistics - add and avoid wrap-around + if (ADDITION_WOULD_WRAP_AROUND(dst->runtime.total_exec_time, cur_cpu_src->runtime.total_exec_time) || + ADDITION_WOULD_WRAP_AROUND(dst->runtime.user_cb_exec_time, cur_cpu_src->runtime.user_cb_exec_time)) { + handle_runtime_stats_wraparound(dst); + } + dst->runtime.total_exec_time += cur_cpu_src->runtime.total_exec_time; + dst->runtime.user_cb_exec_time += cur_cpu_src->runtime.user_cb_exec_time; + dst->runtime.sample_num += cur_cpu_src->runtime.sample_num; + dst->runtime.max_exec_time = MAX(dst->runtime.max_exec_time, cur_cpu_src->runtime.max_exec_time); + dst->runtime.user_cb_max_time = MAX(dst->runtime.user_cb_max_time, cur_cpu_src->runtime.user_cb_max_time); + } + } + return; +} + +#define TOTAL_MICORSEC_TO_AVG_NSEC(total, samples) \ + ((samples)==0 ? 0 : (u_int)((u_int64)(total) * 1000 / (u_int64)(samples))) + +// Print the common statistics +void +kiss_pm_stats_common_print( + kiss_pm_stats_common stats, + enum kiss_pm_stats_type type, + enum kiss_pm_stats_format format, + BOOL print_headline +) +{ + struct kiss_pm_stats_dynamic_s dynamic_stats; + KISS_ASSERT_PERF((stats && !print_headline) || print_headline, ("Illegal arguments")); + + if (type != KISS_PM_DYNAMIC_STATS) { + if (format == KISS_PM_TEXT_FORMAT_STATS) { + kdprintf("Memory comsumption for this handle is %u bytes\n", stats->compile.memory_bytes); + kdprintf("Compilation time for this handle is %u microseconds\n", stats->compile.compilation_time); + } else if (format == KISS_PM_CSV_FORMAT_STATS) { + if (print_headline) { + kdprintf("Memory consumption;Compilation time (microsec);"); + } else { + kdprintf("%u;%u;", stats->compile.memory_bytes, stats->compile.compilation_time); + } + } + } + + if (!print_headline) { + bzero(&dynamic_stats, sizeof(struct kiss_pm_stats_dynamic_s )); + kiss_pm_stats_common_aggregate_cpus(&dynamic_stats, stats); + } + + if (type != KISS_PM_STATIC_STATS) { + if (format == KISS_PM_TEXT_FORMAT_STATS) { + kdprintf("Number of executed buffers is %u\n", dynamic_stats.num_of_buffs); + kdprintf("Max buffer length is %u\n", dynamic_stats.buflen.max); + kdprintf("Avg buffer length is %u\n", + dynamic_stats.buflen.sample_num ? (dynamic_stats.buflen.total/dynamic_stats.buflen.sample_num) : 0); + kdprintf("Number of matches is %u\n", dynamic_stats.num_of_matches); + kdprintf("Number of matches after stage1 is %u\n", dynamic_stats.num_of_stage1_matches); + kdprintf("Number of matches after start-anchor is %u\n", dynamic_stats.num_of_stage22_matches); + kdprintf("Number of matches after end-anchor is %u\n", dynamic_stats.num_of_stage23_matches); + kdprintf("Max number of matches on buffer is %u\n", dynamic_stats.max_matches_on_buf); + // Average execution time - display in nanosecond so rounding down won't lose too much + kdprintf("Avg execution time is %u ns for PM, %u ns for callbacks\n", + TOTAL_MICORSEC_TO_AVG_NSEC(dynamic_stats.runtime.total_exec_time, dynamic_stats.runtime.sample_num), + TOTAL_MICORSEC_TO_AVG_NSEC(dynamic_stats.runtime.user_cb_exec_time, dynamic_stats.runtime.sample_num)); + // Maximum execution time - display in nanosecond for consistency with average. + // concatenate 000 instead of multiplying, + // to avoid overflow (in very extreme, yet very interesting, cases). + kdprintf("Max execution time is %u000 ns for PM, %u000 ns for callbacks\n", + dynamic_stats.runtime.max_exec_time, dynamic_stats.runtime.user_cb_max_time); + } else if (format == KISS_PM_CSV_FORMAT_STATS) { + if (print_headline) { + kdprintf( + "Executed buffers #;" + "Max buffer length;" + "Avg buffer length;" + "Matches #;" + "Max matches on buffer;" + "stage1 matches #;" + "2nd filter matches #;" + "3rd filter matches #;" + "Avg PM exec time (ns);" + "Max PM exec time (ns);" + "Avg callback exec time (ns);" + "Max callback exec time (ns)" + ); + } else { + kdprintf("%u;%u;%u;%u;%u;%u;%u;%u;%u;%u000;%u;%u000", + dynamic_stats.num_of_buffs, + dynamic_stats.buflen.max, + dynamic_stats.buflen.sample_num ? (dynamic_stats.buflen.total/dynamic_stats.buflen.sample_num) : 0, + dynamic_stats.num_of_matches, + dynamic_stats.max_matches_on_buf, + dynamic_stats.num_of_stage1_matches, + dynamic_stats.num_of_stage22_matches, + dynamic_stats.num_of_stage23_matches, + TOTAL_MICORSEC_TO_AVG_NSEC( + dynamic_stats.runtime.total_exec_time, + dynamic_stats.runtime.sample_num + ), + dynamic_stats.runtime.max_exec_time, + TOTAL_MICORSEC_TO_AVG_NSEC( + dynamic_stats.runtime.user_cb_exec_time, + dynamic_stats.runtime.sample_num + ), + dynamic_stats.runtime.user_cb_max_time + ); + } + } + } + + return; +} + +#define kiss_pm_serialize_during_sanity_check 0 + + +// Return the statistics from src in dst (aggregate statistics from all cpus) +kiss_ret_val +kiss_pm_stats_common_get(struct kiss_pm_stats_static_s *dst_compile, + struct kiss_pm_stats_dynamic_s *dst_exec, + const struct kiss_pm_stats_common_s *src) +{ + KISS_ASSERT_PERF((dst_compile && dst_exec && src), ("Illegal arguments")); + + if (!(dst_compile && dst_exec && src)) { + return KISS_ERROR; + } + bzero(dst_compile, sizeof(struct kiss_pm_stats_static_s)); + bzero(dst_exec, sizeof(struct kiss_pm_stats_dynamic_s)); + bcopy(&(src->compile), dst_compile, sizeof(struct kiss_pm_stats_static_s)); + + kiss_pm_stats_common_aggregate_cpus(dst_exec, src); + + // for debug purposes only! + // ignore specific statistics fields when performing a sanity check on serialization + if (kiss_pm_serialize_during_sanity_check) { + dst_compile->memory_bytes = KISS_PM_SERIALIZE_IGNORE_INT; + dst_compile->compilation_time = KISS_PM_SERIALIZE_IGNORE_INT; + } + + return KISS_OK; +} + +// Copy the statistics from src to dst +kiss_ret_val +kiss_pm_stats_common_copy(kiss_pm_stats_common dst, const struct kiss_pm_stats_common_s *src) +{ + if(src && src->exec) { + u_int num_cpus = MIN(src->exec_num_cpus, dst->exec_num_cpus); + KISS_ASSERT_PERF((dst && src), ("Illegal arguments")); + + if (!(dst && src)) { + return KISS_ERROR; + } + bcopy(&(src->compile), &(dst->compile), sizeof(struct kiss_pm_stats_static_s)); + bcopy(src->exec, dst->exec, num_cpus*sizeof(struct kiss_pm_stats_dynamic_aligned_s)); + } + return KISS_OK; +} + +// Get size of serialized common statistics. Only build-time statistics are counted +u_int +kiss_pm_stats_common_get_serialize_size() +{ + return sizeof(struct kiss_pm_stats_static_s); +} + +// Serialize common statistics. Only build-time statistics are serialized +kiss_ret_val +kiss_pm_stats_common_serialize(const struct kiss_pm_stats_common_s *stats, u_char **buf, u_int *size) +{ + KISS_ASSERT_PERF((stats), ("Illegal arguments")); + + DATA_BUFF_COPY(*buf, size, &(stats->compile), sizeof(struct kiss_pm_stats_static_s)); + + return KISS_OK; +} + +// Deserialize common statistics. Only build-time statistics are deserialized +kiss_ret_val +kiss_pm_stats_common_deserialize( + kiss_pm_stats_common stats, + u_char **buf, u_int *size, + CP_MAYBE_UNUSED kiss_vbuf vbuf, + CP_MAYBE_UNUSED kiss_vbuf_iter *vbuf_iter +) +{ + KISS_ASSERT_PERF((stats), ("Illegal arguments")); + + DATA_BUFF_READ(*buf, size, vbuf, *vbuf_iter, &(stats->compile), sizeof(struct kiss_pm_stats_static_s)); + + return KISS_OK; +} + +// ******************** FUNCTIONS ************************* +SASAL_END diff --git a/components/utils/pm/kiss_pm_stats.h b/components/utils/pm/kiss_pm_stats.h new file mode 100644 index 0000000..9f5d36b --- /dev/null +++ b/components/utils/pm/kiss_pm_stats.h @@ -0,0 +1,146 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __kiss_pm_stats_h__ +#define __kiss_pm_stats_h__ + +#include "pm_adaptor.h" + +// Common statistics + +// Common run time statistics +struct kiss_pm_stats_dynamic_s { + u_int num_of_buffs; // Number of buffers we ran this dfa on + u_int num_of_matches; // how many matches there were in this dfa + u_int max_matches_on_buf; // Maximal number of matches per one buf + + struct { // Buffer length statistics + u_int max; // Maximum buffer length + u_int total; // Total length (for average calculation) + u_int sample_num; // Number of buffers, whose lengths make up total. + } buflen; + + struct { // Execution time statistics - not collected by default + u_int total_exec_time; // PM Execution time (not including user callbacks) + u_int max_exec_time; // Maximal PM execution time + u_int user_cb_exec_time; // User callback execution time + u_int user_cb_max_time; // Maximal user callback execution time + u_int sample_num; // Number of execution time samples + } runtime; + + u_int num_of_stage1_matches; // Tier1 LSS matches, before filtering by mask + u_int num_of_stage22_matches; // Tier1 matches after ^ + u_int num_of_stage23_matches; // Tier1 matches after $ +}; + +// Common build time statistics +struct kiss_pm_stats_static_s { + u_int memory_bytes; // How many bytes does this tier consume + u_int compilation_time; // Compilation time of this tier in micro-seconds +}; + +struct CP_CACHELINE_ALIGNED kiss_pm_stats_dynamic_aligned_s { + struct kiss_pm_stats_dynamic_s stats; +}; + +struct kiss_pm_stats_common_s { + // Run time statistics, per-CPU, dynamically allocated + struct kiss_pm_stats_dynamic_aligned_s* exec; + // Size of the exec array + u_int exec_num_cpus; + // Build time statistics + struct kiss_pm_stats_static_s compile; +}; + +typedef struct kiss_pm_stats_common_s *kiss_pm_stats_common; + +enum kiss_pm_stats_update_compile_type { + UPDATE_COMPILE_STATS_MEM, + UPDATE_COMPILE_STATS_TIME, + UPDATE_COMPILE_STATS_BOTH +}; + +// In which format the statistics should be printed +enum kiss_pm_stats_format { + KISS_PM_TEXT_FORMAT_STATS = 0, // Textual, for viewing with text editor + KISS_PM_CSV_FORMAT_STATS // CSV, for opening with Excel +}; + +KISS_APPS_CPAPI +kiss_ret_val kiss_pm_stats_common_init(kiss_pm_stats_common new_stats); + +KISS_APPS_CPAPI +void kiss_pm_stats_common_free(kiss_pm_stats_common stats); + +KISS_APPS_CPAPI +void kiss_pm_stats_common_update_compile( + kiss_pm_stats_common stats, + u_int bytes, + u_int compilation_time, + enum kiss_pm_stats_update_compile_type type); + +KISS_APPS_CPAPI +void kiss_pm_stats_common_update_exec(kiss_pm_stats_common stats, u_int buf_size, u_int num_of_matches); + + +// @brief +// Updating the execution time of an execution of a buffer in tier2. +// +// @param stats - [in] The tier2 common stats. +// @param exec_time - [in] The execution time. +// @param buf_len - [in] the length of the last buffer that was executed +// +// @return Void +// +// @note +// in case one of the stats vars will warp-around, the aggregated vars will hold only the last exec stats. +KISS_APPS_CPAPI +void kiss_pm_stats_common_update_exec_time(kiss_pm_stats_common stats, u_int exec_time, u_int user_cb_time); + +KISS_APPS_CPAPI +void kiss_pm_stats_common_reset_exec(kiss_pm_stats_common stats); + +KISS_APPS_CPAPI +void kiss_pm_stats_common_print( + kiss_pm_stats_common stats, + enum kiss_pm_stats_type type, + enum kiss_pm_stats_format format, + BOOL print_headline +); + +KISS_APPS_CPAPI +kiss_ret_val kiss_pm_stats_common_get( + struct kiss_pm_stats_static_s *dst_compile, + struct kiss_pm_stats_dynamic_s *dst_exec, + const struct kiss_pm_stats_common_s *src +); + +KISS_APPS_CPAPI +kiss_ret_val kiss_pm_stats_common_copy(kiss_pm_stats_common dst, const struct kiss_pm_stats_common_s *src); + +KISS_APPS_CPAPI +u_int kiss_pm_stats_common_get_serialize_size(void); + +KISS_APPS_CPAPI +kiss_ret_val kiss_pm_stats_common_serialize(const struct kiss_pm_stats_common_s *stats, u_char **buf, u_int *size); + +KISS_APPS_CPAPI +kiss_ret_val kiss_pm_stats_common_deserialize( + kiss_pm_stats_common stats, + u_char **buf, + u_int *size, + kiss_vbuf vbuf, + kiss_vbuf_iter *vbuf_iter +); + +#endif // __kiss_pm_stats_h__ diff --git a/components/utils/pm/kiss_thin_nfa.cc b/components/utils/pm/kiss_thin_nfa.cc new file mode 100644 index 0000000..0efee15 --- /dev/null +++ b/components/utils/pm/kiss_thin_nfa.cc @@ -0,0 +1,462 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Thin NFA I/S +// ------------ +// The thin NFA allows building and executing an automaton for string search, using the +// Aho-Corasick algorithm. +// The resulting automaton is built in a compact representation. Some states are "full" - they +// have an explicit transition per character. Others are "partial" - they have some explicit transitions, +// plus a "default transition". This is an epsilon-transition. For characters which don't have an +// explicit transition, we follow the default transition, and look up the same character there. +// +// Source files +// ------------ +// kiss_thin_nfa.c (this file) - execution code. +// kiss_thin_nfa_build.c - allocation and destruction code. Contains code which is common to compilation +// and serialization/deserialization. All objects which are part of the comipled automaton are created here. +// kiss_thin_nfa_compile.c - compilation code. Contains the logic that converts a set of strings into an automaton. +// kiss_thin_nfa_analyze.c - Validation and dump. Code that reads the BNFA and tries to make sense of it. +// kiss_thin_nfa_impl.h - internal header file. APIs and definitions between the different source files. + + +// ********************* INCLUDES ************************** +#include "kiss_thin_nfa_impl.h" +#include "sasal.h" + +SASAL_START // Multiple Pattern Matcher +// Internal execution flags passed to kiss_dfa_exec_one_buf: +#define KISS_PM_EXEC_LAST_BUFF 0x00000001 // This is the last buffer (preset buffer or the last buffer in vbuf) + + +// The runtime status of the Thin NFA +struct kiss_bnfa_runtime_s { + KissThinNFA *nfa_h; // The NFA we're executing + kiss_bnfa_comp_offset_t last_bnfa_offset; // Last state reached by exec_one_buf + std::vector> *matches; // The matches we've found so far + u_int scanned_so_far; // The length of all buffers before the current buffer +}; + + +// Critical code path debugging - enabled only in debug mode. +#define THIN_NFA_TRACE_TRANS(runtime, next_off, ch, op) \ + thinnfa_debug_perf( \ + "%s: Transition by 0x%02x to %d - %s\n", \ + FILE_LINE, \ + ch, \ + kiss_bnfa_offset_decompress(next_off), \ + op \ + ) + +#define TRANSLATE_CHAR_IF_NEEED(do_char_trans, char_trans_table, ch) \ + ((u_char)((do_char_trans) ? ((char_trans_table)[ch]) : (ch))) + +// Given a match for a pattern at a given position, insert an entry to the match list. +// We may add more than one entry, depending on the number of matching patterns. +// +// Parameters: +// runtime - the current status of Thin NFA execution. +// one_buf_offset - the offset of the match, within the buffer currently scanned. +// Together with runtime->scanned_so_far we can get the real match offset. +// one_buf_len - the length of the buffer currently scanned. Used for $ processing. +// exec_flags - the flags used. +static CP_INLINE void +kiss_thin_nfa_handle_match(struct kiss_bnfa_runtime_s *runtime, u_int pat_arr_offset, + u_int one_buf_offset, u_int one_buf_len, u_int exec_flags) +{ + static const char rname[] = "kiss_thin_nfa_handle_match"; + u_int match_pos; + const kiss_thin_nfa_pattern_array_t *pat_arr; + const kiss_thin_nfa_pattern_t *curr_id; + const kiss_thin_nfa_pattern_t *pat_end; + + // Where was the match? one_buf_offset is already moved beyond the characeter that caused the match, + // so we subtract one to get this character's offset. + match_pos = runtime->scanned_so_far + (one_buf_offset - 1); + pat_arr = kiss_thin_nfa_offset_to_pat_array_ptr(runtime->nfa_h, pat_arr_offset); + // Go over the patterns and add them to the match queue. + pat_end = &(pat_arr->pattern[pat_arr->n_patterns]); + thinnfa_debug_perf(( + "%s: Going over %u patterns, starting from offset %u\n", + rname, + pat_arr->n_patterns, + pat_arr_offset + )); + for (curr_id = &(pat_arr->pattern[0]); curr_id != pat_end; curr_id++) { + thinnfa_debug(("%s: Match for pattern ID %d at %d len %d\n", rname, curr_id->id, match_pos, curr_id->len)); + + // Handle ^ - An N byte pattern at the start of the buffer would match at byte N-1. + // NOTE: If the anchored state optimization is implemented in compilation, this test isn't needed. + if ((curr_id->pattern_id_flags & KISS_PM_LSS_AT_BUF_START) && (match_pos != curr_id->len - 1)) { + thinnfa_debug_perf(("%s: Not match because of ^ %d\n", rname, curr_id->id)); + continue; + } + + // Handle $ - We must match at the buffer end, and it must be the last buffer + if ((curr_id->pattern_id_flags & KISS_PM_LSS_AT_BUF_END) && + !((one_buf_offset == one_buf_len) && (exec_flags & KISS_PM_EXEC_LAST_BUFF))) { + thinnfa_debug_perf(("%s: Not match because of $ %d\n", rname, curr_id->id)); + continue; + } + runtime->matches->emplace_back(curr_id->id, match_pos); + } + + return; +} + + +// Wrapper to kiss_thin_nfa_handle_match, gets the state offset, not the ID. +static CP_INLINE void +kiss_thin_nfa_handle_match_state(struct kiss_bnfa_runtime_s *runtime, kiss_bnfa_comp_offset_t cur_offset, + u_int one_buf_offset, u_int one_buf_len, u_int exec_flags) +{ + const kiss_bnfa_state_t *state = kiss_bnfa_comp_offset_to_state( + runtime->nfa_h->bnfa, + cur_offset, + KISS_BNFA_STATE_MATCH + ); + kiss_thin_nfa_handle_match(runtime, state->match.match_id, one_buf_offset, one_buf_len, exec_flags); +} + +// Calculate the next state's offset, given a state and character. Good for full states only. +// Faster than kiss_thin_nfa_get_next_offset. An offset peremeter is compressed 16-bit offset +// The returned offset is also compressed +static CP_INLINE kiss_bnfa_comp_offset_t +kiss_thin_nfa_get_next_offset_full(const kiss_bnfa_state_t *bnfa, kiss_bnfa_comp_offset_t offset, + unsigned char char_to_find) +{ + const kiss_bnfa_state_t *state = kiss_bnfa_comp_offset_to_state(bnfa, offset, KISS_BNFA_STATE_FULL); + return (kiss_bnfa_comp_offset_t)state->full.transitions[char_to_find]; +} + + +// Calculate the next state's offset, given a state and character. Good for partial states only. +// Also indicates whether the buffer position should be incremented (i.e. if an explicit transition was found) +static CP_INLINE kiss_bnfa_comp_offset_t +kiss_thin_nfa_get_next_offset_partial(const kiss_bnfa_state_t *bnfa, kiss_bnfa_comp_offset_t offset, + unsigned char char_to_find, BOOL *inc_pos) +{ + const kiss_bnfa_state_t *state = kiss_bnfa_comp_offset_to_state(bnfa, offset, KISS_BNFA_STATE_PARTIAL); + u_int trans_num = state->partial.trans_num; + u_int i; + + // Simple linear search is fast for a few transitions. If we have many, we use a full state anyway. + for (i = 0; i < trans_num; i++) { + const struct kiss_bnfa_partial_transition_s *tran = &state->partial.transitions[i]; + // Smaller? Keep looking. Larger? Give up (transitions are sorted). + if (tran->tran_char < char_to_find) continue; + if (tran->tran_char > char_to_find) break; + + // Found the character (explicit transition) - consume a characeter and move the automaton + *inc_pos = TRUE; + return tran->next_state_offset; + } + + // No explicit transition found - move to the fail state, without consuming a character. + *inc_pos = FALSE; + return state->partial.fail_state_offset; +} + + +// Calculate the next state's offset, when the current is a match state. +// Doesn't consume a character (epsilon transition) +static CP_INLINE kiss_bnfa_comp_offset_t +kiss_thin_nfa_get_next_offset_match(CP_MAYBE_UNUSED const kiss_bnfa_state_t *bnfa, kiss_bnfa_comp_offset_t offset) +{ + // After a match state we just move to the next consecutive state. + return offset + (sizeof(kiss_bnfa_match_state_t) / KISS_BNFA_STATE_ALIGNMENT); +} + +#define PARALLEL_SCANS_NUM 4 // 4 heads scanning the buffer +#define UNROLL_FACTOR 4 // Advance each head 4 bytes per loop + + +// Move one head of the state machine. bnfa_offset must not be a match state. +static CP_INLINE kiss_bnfa_comp_offset_t +parallel_scan_advance_one(const kiss_bnfa_state_t *bnfa, kiss_bnfa_comp_offset_t bnfa_offset, const unsigned char ch) +{ + while (bnfa_offset >= 0) { + BOOL inc_pos; + // Partial state - Look for an explicit transition, or use the fail state + bnfa_offset = kiss_thin_nfa_get_next_offset_partial(bnfa, bnfa_offset, ch, &inc_pos); + if (inc_pos) { + // Found an explicit transition - can move to the next state. + return bnfa_offset; + } + } + + // Full state (either we started with full, or the fail state chain reached one) + return kiss_thin_nfa_get_next_offset_full(bnfa, bnfa_offset, ch); +} + + +// Check if all heads are on a full state. +// If they are - advance all heads and return TRUE. +// If they aren't - do nothing and return FALSE. +static CP_INLINE BOOL +parallel_scan_advance_if_full( + const kiss_bnfa_state_t *bnfa, + kiss_bnfa_comp_offset_t *bnfa_offsets, + const unsigned char **buf_pos +) +{ + kiss_bnfa_comp_offset_t offsets_and; + + // If the bitwise AND of 4 offsets (PARALLEL_SCANS_NUM) is negative, they're all negaitve, so all states are full. + offsets_and = bnfa_offsets[0] & bnfa_offsets[1] & bnfa_offsets[2] & bnfa_offsets[3]; + if (CP_UNLIKELY(offsets_and >= 0)) return FALSE; + + // All states are full - make 4 transitions (PARALLEL_SCANS_NUM). + bnfa_offsets[0] = kiss_thin_nfa_get_next_offset_full(bnfa, bnfa_offsets[0], *(buf_pos[0])); + buf_pos[0]++; + bnfa_offsets[1] = kiss_thin_nfa_get_next_offset_full(bnfa, bnfa_offsets[1], *(buf_pos[1])); + buf_pos[1]++; + bnfa_offsets[2] = kiss_thin_nfa_get_next_offset_full(bnfa, bnfa_offsets[2], *(buf_pos[2])); + buf_pos[2]++; + bnfa_offsets[3] = kiss_thin_nfa_get_next_offset_full(bnfa, bnfa_offsets[3], *(buf_pos[3])); + buf_pos[3]++; + + return TRUE; +} + + +// Repeat parallel_scan_advance_if_full up to 4 times (UNROLL_FACTOR). +// Retrurn TRUE if all 4 were done, FALSE if stopped earlier. +static CP_INLINE BOOL +parallel_scan_advance_if_full_unroll( + const kiss_bnfa_state_t *bnfa, + kiss_bnfa_comp_offset_t *bnfa_offsets, + const unsigned char **buf_pos +) +{ + if (!parallel_scan_advance_if_full(bnfa, bnfa_offsets, buf_pos)) return FALSE; + if (!parallel_scan_advance_if_full(bnfa, bnfa_offsets, buf_pos)) return FALSE; + if (!parallel_scan_advance_if_full(bnfa, bnfa_offsets, buf_pos)) return FALSE; + if (!parallel_scan_advance_if_full(bnfa, bnfa_offsets, buf_pos)) return FALSE; + return TRUE; +} + + +// Find the offset where each head should start and stop +static void +calc_head_buf_range(const u_char *buffer, u_int len, const u_char **head_start_pos, const u_char **head_end_pos) +{ + static const char rname[] = "calc_head_buf_range"; + const u_char *orig_buf = buffer; + u_int len_per_head = len / PARALLEL_SCANS_NUM; + u_int rem = len % PARALLEL_SCANS_NUM; + u_int i; + + for (i=0; i= PARALLEL_SCANS_NUM-rem) head_len++; + head_start_pos[i] = buffer; + buffer += head_len; + head_end_pos[i] = buffer; + thinnfa_debug(("%s: Head %u gets range %ld:%ld\n", rname, + i, head_start_pos[i]-orig_buf, head_end_pos[i]-orig_buf)); + } +} + +// Set the initial BNFA offset for each head +static void +set_head_bnfa_offset( + struct kiss_bnfa_runtime_s *runtime, + kiss_bnfa_comp_offset_t *bnfa_pos, + const u_char **buf_pos, + const u_char *buffer +) +{ + const KissThinNFA *nfa_h = runtime->nfa_h; + kiss_bnfa_comp_offset_t init_off = kiss_bnfa_offset_compress(nfa_h->min_bnfa_offset); + u_int i; + + if (nfa_h->flags & KISS_THIN_NFA_HAS_ANCHOR) { + // Start from the root (next full state after the anchored root) + init_off++; + } + + // Heads that scan from the beginning of the buffer, will start at previous buffer's ending state. + // The rest start anew. + // Several scanning heads will start at buffer's beginning when buffer's size is less than PARALLEL_SCANS_NUM + for (i=0; ilast_bnfa_offset; + } else { + bnfa_pos[i] = init_off; + } + } +} + + +// Run Thin NFA parallely on a single buffer. +static CP_INLINE void +kiss_thin_nfa_exec_one_buf_parallel_ex( + struct kiss_bnfa_runtime_s *runtime, + const u_char *buffer, + u_int len, u_int flags, + BOOL do_char_trans, + u_char *char_trans_table +) +{ + const kiss_bnfa_state_t *bnfa = runtime->nfa_h->bnfa; + const unsigned char *end, *buf_pos[PARALLEL_SCANS_NUM], *head_end_pos[PARALLEL_SCANS_NUM]; + kiss_bnfa_comp_offset_t bnfa_offset[PARALLEL_SCANS_NUM]; + u_int i; + u_int overlap_bytes; + int overlap_head_mask; + + // set starting position, ending position and state for each scanning head + calc_head_buf_range(buffer, len, buf_pos, head_end_pos); + set_head_bnfa_offset(runtime, bnfa_offset, buf_pos, buffer); + + end = buffer + len; + + // unroll 16 (PARALLEL_SCANS_NUM * UNROLL_FACTOR) times, while we have at least 4 input bytes to process. + while (buf_pos[PARALLEL_SCANS_NUM-1] + UNROLL_FACTOR <= end) { + // Fastpath - Advance all heads up to 4 chars, as long as they're all on a full state. + if (CP_LIKELY(parallel_scan_advance_if_full_unroll(bnfa, bnfa_offset, buf_pos))) continue; + + // At least one head is on partial or match - advance all 4 by their type. + for (i=0; i= head_end_pos[i]) continue; + if (kiss_bnfa_state_type(bnfa, bnfa_offset[i]) == KISS_BNFA_STATE_MATCH) { + // Handle a match + kiss_thin_nfa_handle_match_state(runtime, bnfa_offset[i], (u_int)(buf_pos[i] - buffer), len, flags); + bnfa_offset[i] = kiss_thin_nfa_get_next_offset_match(bnfa, bnfa_offset[i]); + } + // Advance to the next state + bnfa_offset[i] = parallel_scan_advance_one(bnfa, bnfa_offset[i], + TRANSLATE_CHAR_IF_NEEED(do_char_trans, char_trans_table, *(buf_pos[i]))); + (buf_pos[i])++; + } + } + + // Handle overlap - advance all heads into the next head's range, as long as there's a chance + // for a match which started in this head's range. + overlap_head_mask = (1<<(PARALLEL_SCANS_NUM-1))-1; // All heads except the last + for (overlap_bytes = 0; overlap_head_mask!=0; overlap_bytes++) { + // Advance each head (except the last) as long as overlap is needed for it + for (i=0; infa_h, bnfa_offset[i]); + if ((state_depth <= overlap_bytes) || (buf_pos[i] >= end)) { + overlap_head_mask &= ~my_mask; + continue; + } + + // Advance the state machine, including match handling + if (kiss_bnfa_state_type(bnfa, bnfa_offset[i]) == KISS_BNFA_STATE_MATCH) { + // Handle a match + kiss_thin_nfa_handle_match_state(runtime, bnfa_offset[i], (u_int)(buf_pos[i] - buffer), len, flags); + bnfa_offset[i] = kiss_thin_nfa_get_next_offset_match(bnfa, bnfa_offset[i]); + } + // Advance to the next state + bnfa_offset[i] = parallel_scan_advance_one(bnfa, bnfa_offset[i], + TRANSLATE_CHAR_IF_NEEED(do_char_trans, char_trans_table, *(buf_pos[i]))); + (buf_pos[i])++; + } + } + + // We may have stopped on a match state. If so - handle and advance + for (i=0; ilast_bnfa_offset = bnfa_offset[i]; + break; + } + } + + return; +} + + +// Execute a thin NFA on a buffer. +// Parameters: +// nfa_h - the NFA handle +// buf - a buffer to scan. +// matches - output - will be filled with a kiss_pmglob_match_data element for each match. +void +kiss_thin_nfa_exec(KissThinNFA *nfa_h, const Buffer& buf, std::vector> &matches) +{ + struct kiss_bnfa_runtime_s bnfa_runtime; + + dbgAssert(nfa_h != nullptr) << "kiss_thin_nfa_exec() was called with null handle"; + + if (buf.size() == 0) { + return; + } + + // Set the runtime status structure + bnfa_runtime.nfa_h = nfa_h; + bnfa_runtime.last_bnfa_offset = kiss_bnfa_offset_compress(nfa_h->min_bnfa_offset); // The initial state + bnfa_runtime.matches = &matches; + bnfa_runtime.scanned_so_far = 0; + + auto segments = buf.segRange(); + for( auto iter = segments.begin(); iter != segments.end(); iter++ ) { + const u_char * data = iter->data(); + u_int len = iter->size(); + u_int flags = ((iter+1)==segments.end()) ? KISS_PM_EXEC_LAST_BUFF : 0; + if (nfa_h->flags & KISS_THIN_NFA_USE_CHAR_XLATION) { + kiss_thin_nfa_exec_one_buf_parallel_ex(&bnfa_runtime, data, len, flags, TRUE, nfa_h->xlation_tab); + } else { + kiss_thin_nfa_exec_one_buf_parallel_ex(&bnfa_runtime, data, len, flags, FALSE, nullptr); + } + bnfa_runtime.scanned_so_far += len; + } + + return; +} +SASAL_END diff --git a/components/utils/pm/kiss_thin_nfa_analyze.cc b/components/utils/pm/kiss_thin_nfa_analyze.cc new file mode 100644 index 0000000..d2c5046 --- /dev/null +++ b/components/utils/pm/kiss_thin_nfa_analyze.cc @@ -0,0 +1,1499 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include "pm_adaptor.h" +#include "kiss_thin_nfa_impl.h" +#include "kiss_hash.h" +#include "sasal.h" + +SASAL_START // Multiple Pattern Matcher +#define hash_t kiss_hash_t +#define hash_intcreate_ex(sz, kmem_flags) \ + kiss_hash_create(sz, (hkeyfunc_t)kiss_hash_intvalue, (hcmpfunc_t)kiss_hash_intcmp, NULL) +#define hash_lookup kiss_hash_lookup +#define hash_insert kiss_hash_insert +#define hash_find_hashent kiss_hash_find_hashent +#define hashent kiss_hashent +#define hash_iterator_create kiss_hash_iterator_create +#define hash_iterator_get_hashent kiss_hash_iterator_get_hashent +#define hash_iterator kiss_hash_iterator +#define hash_iterator_next_ent kiss_hash_iterator_next_ent +#define hash_iterator_destroy kiss_hash_iterator_destroy +#define hash_destroy kiss_hash_destroy + +// Thin NFA validation code +// ------------------------ +// We validate the following things: +// 1. For each state: +// a. That it's within the BNFA bounds. +// b. If matching, the pattern offset is valid. +// 2. For each transition: +// a. That it points to a valid state. +// b. For partial states, that the normal transitions point down the tree, and fail states point up. +// 3. For the entire tree, that all states are in it. +// 4. The pattern array is valid, and all offsets are used at least once. +typedef enum { + VALIDATION_STAT_FLAG_NONE = 0x00, + VALIDATION_STATE_IS_JUMP = 0x01, + VALIDATION_STATE_IS_ANCHORED = 0x02, + VALIDATION_STATE_BNFA_ONLY = 0x04, // Match/jump states, not a real part of the tree +} validation_state_flags_t; + + +// Information we keep about a state while validating the Thin NFA +struct state_validation_data_s { + kiss_bnfa_offset_t bnfa_offset; + int level; + struct state_validation_data_s *next; + struct state_validation_data_s *parent_state; // First arrived here from this state + u_char trans_char; // First arrived here by transition using this character + kiss_bnfa_state_type_t type; + validation_state_flags_t flags; + struct thinnfa_validation_status_s *validation; // The global validation data +}; + + +// In which direction do we expect a transition to be? +typedef enum { + TRANS_DIRECTION_BACK, // Fail state - must be a lower tier + TRANS_DIRECTION_FAIL_ONLY, // Fail state of a state with not transition - leaf or jump state + TRANS_DIRECTION_FORWARD, // Partial state transition - must be a higher tier + TRANS_DIRECTION_ANY // Full state trnsition - we can't tell +} transition_direction_t; + + +struct thinnfa_validation_status_s { + const KissThinNFA *nfa; // The NFA being validated + struct state_validation_data_s *state_data; // Validation info per state + struct state_validation_data_s *root, *anchored_root; // Interesting special states + u_int state_num; // States we found in the automaton + hash_t offset_to_data; // BNFA offset -> &state_data[i] + struct state_validation_data_s *q_head, *q_tail; // Queue for BFS scan + hash_t pat_array_offset_ref_count; // Offset in the pattern array -> + // number of finite states using it +}; + + +// Callbacks for PM dump +typedef enum { + THIN_NFA_DUMP_FLAGS_NONE = 0x00, + THIN_NFA_DUMP_SKIP_ROOT_TRANS = 0x01, +} thin_nfa_dump_flags_t; + +// Callbacks provided by different dump formats +typedef struct { + void (*start_cb) (const struct thinnfa_validation_status_s *validation); + void (*state_start_cb) (const struct state_validation_data_s *state_data); + void (*transition_cb) (const struct state_validation_data_s *from_state, + u_char tran_char, + kiss_bnfa_offset_t next_state_off); + void (*state_end_cb) (const struct state_validation_data_s *state_data); + void (*end_cb) (const struct thinnfa_validation_status_s *validation); + thin_nfa_dump_flags_t flags; +} thin_nfa_dump_cbs_t; + +// Change name printing to compensate for annoying Wiki behavior with backslashes: +// "\\\x" is printed as "\x". +// There's no safe way to print a single backslash: "\\" is nothing, "\\\\" is "\\". +// You can try "\\\", which works, unless it's at the end of the string. +static int doing_wiki_dump = 0; + +// Change name printing to avoid various chars which confuse Excel in CSVs +static int doing_csv_dump = 0; + +static int +is_csv_printable(u_char c) +{ + return !( c == '\\' || c == ',' || c == '\'' || c=='"' || c=='=' || c==' ' || c=='+' || c=='-'); +} + + +// Get a printable representation of a character, suitable for inclusion in double quotes. +static const char * +char_to_printable(u_char ch) +{ + static char buf[8]; + + if (!isprint(ch) || ch==' ' || (doing_wiki_dump && ch=='\\') || (doing_csv_dump && !is_csv_printable(ch))) { + // Print the hex value if not printable + snprintf(buf, sizeof(buf), "%sx%02X", doing_wiki_dump ? "\\\\\\" : "\\", ch); + } else if (ch == '"' || ch == '\\') { + // Escape " and \ so they will behave nicely in a double-quoted string + snprintf(buf, sizeof(buf), "\\%c", ch); + } else { + // Just print the character + snprintf(buf, sizeof(buf), "%c", ch); + } + return buf; +} + + +static struct state_validation_data_s * +thin_nfa_validation_offset_to_state(struct thinnfa_validation_status_s *validation, kiss_bnfa_offset_t bnfa_offset) +{ + return (struct state_validation_data_s *)hash_lookup(validation->offset_to_data, (void *)(intptr_t)bnfa_offset); +} + +#define NUM_NAME_BUFS 3 +#define NAME_BUF_LEN 50 + +// Generate a nice printable name for the state +static const char * +state_name(const struct state_validation_data_s *state) +{ + struct thinnfa_validation_status_s *validation = state->validation; + static char name_bufs[NAME_BUF_LEN][NUM_NAME_BUFS]; + static int cur_name_buf = 0; + char *name_buf; + char *p; + const struct state_validation_data_s *tmp_state; + + // Special cases + if (state == validation->root) return "ROOT"; + if (state == validation->anchored_root) return "^ROOT"; + + // Follow transitions backwards, build the name + name_buf = name_bufs[cur_name_buf]; + cur_name_buf = (cur_name_buf+1)%NUM_NAME_BUFS; + p=&name_buf[NAME_BUF_LEN-1]; + *p = '\0'; + + if (!doing_csv_dump && state->flags&VALIDATION_STATE_BNFA_ONLY) { + // Mark matching and jump states with a suffix. + p--; + *p = (state->flags&VALIDATION_STATE_IS_JUMP) ? '#' : '*'; + } + + // Follow transitions backwards, build the name + for (tmp_state=state; tmp_state!=validation->root; tmp_state=tmp_state->parent_state) { + const char *ctext; + if (tmp_state->parent_state == NULL) { + // Possible if we haven't iterated all states yet. Just give the BNFA offset + snprintf(name_buf, NAME_BUF_LEN, "STATE_%d", state->bnfa_offset); + return name_buf; + } + + if (tmp_state->parent_state->flags&VALIDATION_STATE_BNFA_ONLY) { + // The characters are reported in real states only + continue; + } + + // Add the transition character to the name. Make sure not to add \0. + ctext = char_to_printable(tmp_state->trans_char); + if (p < name_buf+strlen(ctext)) break; + p -= strlen(ctext); + bcopy(ctext, p, strlen(ctext)); + } + + if (tmp_state != validation->root) { + // Didn't go back to the root - add ? prefix + if (p > name_buf) p--; + *p = '?'; + } + return p; +} + + +// Return a state's epsilon transition, or KISS_BNFA_OFFSET_INVALID if none. +static kiss_bnfa_offset_t +validation_state_epsilon_trans(const struct state_validation_data_s *state_data) +{ + switch (state_data->type) { + case KISS_BNFA_STATE_PARTIAL: { + const kiss_bnfa_state_t *state = kiss_bnfa_offset_to_state(state_data->validation->nfa->bnfa, + state_data->bnfa_offset); + return kiss_bnfa_offset_decompress(state->partial.fail_state_offset); + } + case KISS_BNFA_STATE_MATCH: + return state_data->bnfa_offset + sizeof(kiss_bnfa_match_state_t); + + case KISS_BNFA_STATE_FULL: + default: + return KISS_BNFA_OFFSET_INVALID; + } +} + + +// How many outgoing transitions do we have? +static u_int +validation_state_trans_num(const struct state_validation_data_s *state_data) +{ + switch (state_data->type) { + case KISS_BNFA_STATE_FULL: return KISS_PM_ALPHABET_SIZE; + case KISS_BNFA_STATE_MATCH: return 0; + case KISS_BNFA_STATE_PARTIAL: { + const kiss_bnfa_state_t *state = kiss_bnfa_offset_to_state(state_data->validation->nfa->bnfa, + state_data->bnfa_offset); + return state->partial.trans_num; + } + + case KISS_BNFA_STATE_TYPE_NUM: return 0; + } + return 0; +} + + +static void +thin_nfa_validation_queue_enq(struct state_validation_data_s *item) +{ + struct thinnfa_validation_status_s *validation = item->validation; + // Add at the tail. Set the existing tail (if any) or head (if not) to point to the new item. + if (validation->q_tail != NULL) { + validation->q_tail->next = item; + } else { + validation->q_head = item; + } + validation->q_tail = item; + item->next = NULL; +} + + +static void +thin_nfa_validation_queue_enq_head(struct state_validation_data_s *item) +{ + struct thinnfa_validation_status_s *validation = item->validation; + item->next = validation->q_head; + validation->q_head = item; + if (validation->q_tail==NULL) { + validation->q_tail = item; + } +} + + +static struct state_validation_data_s * +thin_nfa_validation_queue_deq(struct thinnfa_validation_status_s *validation) +{ + struct state_validation_data_s *item; + + // Remove from the head + item = validation->q_head; + if (!item) return NULL; + + validation->q_head = item->next; + if (validation->q_head == NULL) { + // Removed last + validation->q_tail = NULL; + } + item->next = NULL; + return item; +} + + +// Is a state within the BNFA boundaries? +static kiss_ret_val +thin_nfa_validate_offset_in_range(const KissThinNFA *nfa_handle, kiss_bnfa_offset_t bnfa_offset, u_int state_size, + const char *caller, const char *msg) +{ + if ((bnfa_offset >= nfa_handle->min_bnfa_offset) && (bnfa_offset+(int)state_size <= nfa_handle->max_bnfa_offset)) { + return KISS_OK; + } + thinnfa_debug_critical(("%s: State at BNFA offset %d %s %d - out of range (%d:%d)\n", + caller, bnfa_offset, + msg, state_size, nfa_handle->min_bnfa_offset, nfa_handle->max_bnfa_offset)); + return KISS_ERROR; +} + + +// Validate the state, which is at a given BNFA offset, is within the BNFA boundaries +static kiss_ret_val +thin_nfa_validate_state_in_range(const KissThinNFA *nfa_handle, kiss_bnfa_offset_t bnfa_offset, u_int *state_size_p) +{ + static const char rname[] = "thin_nfa_validate_state_in_range"; + u_int state_size; + + // See that the basic header fits, so we don't read outside the BNFA + if (thin_nfa_validate_offset_in_range(nfa_handle, bnfa_offset, sizeof(kiss_bnfa_minimal_state_t), + rname, "header") != KISS_OK) { + return KISS_ERROR; + } + + // Find the state's real size + state_size = kiss_bnfa_state_size(nfa_handle->bnfa, bnfa_offset); + + // See that the whole state fits in + if (thin_nfa_validate_offset_in_range(nfa_handle, bnfa_offset, state_size, rname, "size") != KISS_OK) { + return KISS_ERROR; + } + + *state_size_p = state_size; + return KISS_OK; +} + + +// Find the root and anchored root states +static kiss_ret_val +thin_nfa_validation_find_root(struct thinnfa_validation_status_s *validation) +{ + static const char rname[] = "thin_nfa_validation_find_root"; + kiss_bnfa_offset_t init_offset = validation->nfa->min_bnfa_offset; + struct state_validation_data_s *initial; + + initial = thin_nfa_validation_offset_to_state(validation, init_offset); + if (!initial) { + thinnfa_debug_critical(("%s: Initial state (offset %d) not found\n", rname, init_offset)); + return KISS_ERROR; + } + + if (validation->nfa->flags & KISS_THIN_NFA_HAS_ANCHOR) { + // The initial is the anchored root, the real root immediatey follows + kiss_bnfa_offset_t root_offset = init_offset + sizeof(kiss_bnfa_full_state_t); + struct state_validation_data_s *root = thin_nfa_validation_offset_to_state(validation, root_offset); + if (!root) { + thinnfa_debug_critical(("%s: Failed to find root (offset %u)\n", rname, root_offset)); + return KISS_ERROR; + } + validation->root = root; + validation->anchored_root = initial; + } else { + // No anchored root, the root is initial + validation->root = initial; + validation->anchored_root = NULL; + } + + thinnfa_debug(("%s: BNFA at %p, root %p anchored root %p\n", rname, validation->nfa->bnfa, + validation->nfa->bnfa + validation->root->bnfa_offset, + validation->anchored_root ? validation->nfa->bnfa + validation->anchored_root->bnfa_offset : NULL)); + + return KISS_OK; +} + + +// Set validation->state_num (so we can allocate validation data) +static kiss_ret_val +thin_nfa_validation_count_states(struct thinnfa_validation_status_s *validation) +{ + static const char rname[] = "thin_nfa_validation_count_states"; + kiss_bnfa_offset_t bnfa_offset; + + bnfa_offset = validation->nfa->min_bnfa_offset; + validation->state_num = 0; + while (bnfa_offset < validation->nfa->max_bnfa_offset) { + u_int state_size; + if (thin_nfa_validate_state_in_range(validation->nfa, bnfa_offset, &state_size) != KISS_OK) return KISS_ERROR; + validation->state_num++; + bnfa_offset += state_size; + } + + thinnfa_debug(("%s: Found %d states\n", rname, validation->state_num)); + + return KISS_OK; +} + + +// Go over all states and fill in validation data structure. +// Doesn't fill in the level - saved for a later BFS iteration. +static kiss_ret_val +thin_nfa_validation_find_states(struct thinnfa_validation_status_s *validation) +{ + static const char rname[] = "thin_nfa_validation_find_states"; + u_int i; + kiss_bnfa_offset_t bnfa_offset; + u_int states_added = 0; + + thinnfa_debug(("%s: Validating %p\n", rname, validation->nfa)); + + bnfa_offset = validation->nfa->min_bnfa_offset; + for (i = 0; i < validation->state_num; i++) { + struct state_validation_data_s *state_data; + const kiss_bnfa_state_t *state; + kiss_bnfa_state_type_t type; + u_int state_size; + u_int req_alignment; + + // See that the state fits in the BNFA + if (thin_nfa_validate_state_in_range(validation->nfa, bnfa_offset, &state_size) != KISS_OK) return KISS_ERROR; + type = kiss_bnfa_state_type(validation->nfa->bnfa, kiss_bnfa_offset_compress(bnfa_offset)); + state = kiss_bnfa_offset_to_state(validation->nfa->bnfa, bnfa_offset); + + thinnfa_debug_extended(("%s: State %u offset %d type %d size %d\n", rname, i, bnfa_offset, type, state_size)); + if (type == KISS_BNFA_STATE_MATCH) { + thinnfa_debug_extended(("%s: pattern array offset %u\n", rname, state->match.match_id)); + } + + // Verify that the offset and type agree + switch (type) { + case KISS_BNFA_STATE_FULL: + if (bnfa_offset >= 0) { + thinnfa_debug_critical(("%s: Full state at positive offset %d\n", rname, bnfa_offset)); + return KISS_ERROR; + } + req_alignment = sizeof(kiss_bnfa_full_state_t); + break; + + case KISS_BNFA_STATE_MATCH: + case KISS_BNFA_STATE_PARTIAL: + if (bnfa_offset < 0) { + // Can't really happen, because kiss_bnfa_state_type would return KISS_BNFA_STATE_FULL. + thinnfa_debug_critical(("%s: State type %d at negative offset %d\n", rname, type, bnfa_offset)); + return KISS_ERROR; + } + req_alignment = KISS_BNFA_STATE_ALIGNMENT; + break; + + default: + thinnfa_debug_critical(("%s: Invalid state type at offset %d - %d\n", rname, bnfa_offset, type)); + return KISS_ERROR; + } + + // Verify that the offset is properly aligned + if ((bnfa_offset % req_alignment) != 0) { + thinnfa_debug_critical(("%s: State offset %d - type %d but not on %d boundary\n", rname, + bnfa_offset, type, req_alignment)); + return KISS_ERROR; + } + + // OK - remember the state and advance the offset + state_data = &validation->state_data[states_added]; + states_added++; + + state_data->bnfa_offset = bnfa_offset; + state_data->next = NULL; + state_data->level = -1; // Indicating not visited. We'll calculate it during BFS traversal. + state_data->parent_state = NULL; // No parent, yet (will stay this way for the root) + state_data->trans_char = '\0'; // Meaningless when there's no parent_state + state_data->type = type; + state_data->flags = VALIDATION_STAT_FLAG_NONE; + state_data->validation = validation; + + if (type == KISS_BNFA_STATE_MATCH) { + ENUM_SET_FLAG(state_data->flags, VALIDATION_STATE_BNFA_ONLY); + } + + if (hash_insert(validation->offset_to_data, (void *)(intptr_t)bnfa_offset, state_data) == 0) { + // XXX: Failing verification on memory allocation error. Not nice. + // Can first build the hash, without any verifications, and only then verify. + thinnfa_debug_critical(("%s: validation hash insert %d->%p failed\n", rname, bnfa_offset, state_data)); + return KISS_ERROR; + } + bnfa_offset += state_size; + } + + if (bnfa_offset != validation->nfa->max_bnfa_offset) { + thinnfa_debug_critical(("%s: Found %d of %d states, reached offset %d, not %d\n", rname, + states_added, validation->state_num, bnfa_offset, validation->nfa->max_bnfa_offset)); + return KISS_ERROR; + } + + // Set pointers to root states + if (thin_nfa_validation_find_root(validation) != KISS_OK) return KISS_ERROR; + + return KISS_OK; +} + + +// Follow a transition, by adding the next state to the scan list, if it wasn't added yet. +static void +thin_nfa_validation_add_next_state( + struct state_validation_data_s *from_state_data, + struct state_validation_data_s *next_state_data, + u_char trans_char +) +{ + const KissThinNFA *nfa; + int inc_level; + + if (next_state_data->level >= 0) { + // We've already seen this state + return; + } + + if (from_state_data->flags&VALIDATION_STATE_BNFA_ONLY) { + // A matching state and the following real state are on the same level. + // Using the mathing state's incoming transition char makes the state name end up nice. + inc_level = 0; + trans_char = from_state_data->trans_char; + } else { + inc_level = 1; + } + + if (from_state_data->flags & VALIDATION_STATE_IS_ANCHORED) { + ENUM_SET_FLAG(next_state_data->flags, VALIDATION_STATE_IS_ANCHORED); + } + + nfa = from_state_data->validation->nfa; + if (nfa->flags & KISS_THIN_NFA_USE_CHAR_XLATION) { + // Use the canonic character. Without it, states pointed from partial state get the lowercase char, + // but states pointed from full states get the uppercase (because it's first) + trans_char = nfa->xlation_tab[trans_char]; + } + + // Calculate the level and enqueue + next_state_data->level = from_state_data->level + inc_level; + next_state_data->parent_state = from_state_data; + next_state_data->trans_char = trans_char; + if (inc_level) { + thin_nfa_validation_queue_enq(next_state_data); + } else { + //We want this one to be scanned immediately. + thin_nfa_validation_queue_enq_head(next_state_data); + } +} + + +// Check that a character's transition is to a valid state. +// Returns the next state's data, or NULL if invalid. +static kiss_ret_val +thin_nfa_validation_add_transition(struct state_validation_data_s *prev_state_data, + u_int trans_char, kiss_bnfa_offset_t next_state_offset, transition_direction_t expected_dir) +{ + static const char rname[] = "thin_nfa_validation_add_transition"; + struct state_validation_data_s *next_state_data; + const char *err_msg = NULL; + + // See that there's a state at the target offset + next_state_data = thin_nfa_validation_offset_to_state(prev_state_data->validation, next_state_offset); + if (!next_state_data) { + thinnfa_debug_critical(( + "%s: Transition from '%s' by %02x expected direction %d -> BNFA offset %d - no such state", + rname, + state_name(prev_state_data), trans_char, expected_dir, next_state_offset + )); + return KISS_ERROR; + } + + // Check that transitions are in the direction we expect + switch (expected_dir) { + case TRANS_DIRECTION_FORWARD: + // Partial state explicit transition - must point to a state we've never seen before + if (next_state_data->level >= 0) { + err_msg = "must be a new fail state"; + } + break; + + case TRANS_DIRECTION_BACK: + // Fail state transition - must point to a state we've already seen, on a lower level + if (next_state_data->level < 0) { + err_msg = "transition to an unknown state"; + } else if (next_state_data->level >= prev_state_data->level) { + err_msg = "transition to a higher level"; + } else if (next_state_data->type == KISS_BNFA_STATE_MATCH) { + err_msg = "transition to match the state"; + } + break; + + case TRANS_DIRECTION_FAIL_ONLY: + // Fail state of a state with no transitions. Can be either: + // Leaf state - we expect a transition to a known lower-level state. + // Jump state - we expect a transition to a new full state. + if (next_state_data->level < 0) { + // Jump state. Remember this, so we won't increment the next state's level + ENUM_SET_FLAG(prev_state_data->flags, VALIDATION_STATE_IS_JUMP); + ENUM_SET_FLAG(prev_state_data->flags, VALIDATION_STATE_BNFA_ONLY); + if (next_state_offset >= 0) { + // Jump states are meant to jump to full states. + err_msg = "Jump state to partial"; + } + } else { + // Leaf state + if (next_state_data->level >= prev_state_data->level) { + // A state's fail state must be at a lower level + err_msg = "transition to a level higher than the leaf state"; + } + } + if (err_msg==NULL && next_state_data->type == KISS_BNFA_STATE_MATCH) { + err_msg = "transition to match the leaf state"; + } + break; + + case TRANS_DIRECTION_ANY: + // Full state transition - can point anywhere + break; + } + + if (err_msg != NULL) { + thinnfa_debug_critical(( + "%s: Transition from '%s' by %02x expected dir %d -> '%s', levels %d -> %d, %s\n", + rname, + state_name(prev_state_data), + trans_char, + expected_dir, + state_name(next_state_data), + prev_state_data->level, + next_state_data->level, + err_msg + )); + return KISS_ERROR; + } + + // Add the next state to the tree + thin_nfa_validation_add_next_state(prev_state_data, next_state_data, (u_char)trans_char); + + return KISS_OK; +} + + +// Do a BFS scan of the tree and check transitions +static kiss_ret_val +thin_nfa_validation_scan_tree(struct thinnfa_validation_status_s *validation) +{ + static const char rname[] = "thin_nfa_validation_scan_tree"; + kiss_ret_val ret; + + // Initialize scan list with the root. The list contains all states, whose level + // was already calculated, but whose children were not examined yet. + validation->q_head = validation->q_tail = NULL; + validation->root->level = 0; + thin_nfa_validation_queue_enq(validation->root); + + if (validation->anchored_root) { + // ^ROOT behaves like ROOT's child, setting level 1 makes fail transitions to normal tree OK + validation->anchored_root->level = 1; + validation->anchored_root->parent_state = validation->root; + validation->anchored_root->trans_char = '^'; // Makes printing the name nice + ENUM_SET_FLAG(validation->anchored_root->flags, VALIDATION_STATE_IS_ANCHORED); + thin_nfa_validation_queue_enq(validation->anchored_root); + } + + // No errors yet + ret = KISS_OK; + + while (1) { + struct state_validation_data_s *state_data; + const kiss_bnfa_state_t *state; + + // Remove an element from the list + state_data = thin_nfa_validation_queue_deq(validation); + if (!state_data) break; + + state = kiss_bnfa_offset_to_state(validation->nfa->bnfa, state_data->bnfa_offset); + + switch (state_data->type) { + case KISS_BNFA_STATE_PARTIAL: { + // Partial State + const kiss_bnfa_partial_state_t *p_state = &state->partial; + u_int i; + + // Check the fail state (tran_char=0 is because its meaningless) + if (thin_nfa_validation_add_transition(state_data, 0, validation_state_epsilon_trans(state_data), + p_state->trans_num==0 ? TRANS_DIRECTION_FAIL_ONLY : TRANS_DIRECTION_BACK) != KISS_OK) { + ret = KISS_ERROR; + } + + // Go over the transitions to all included characters + for (i=0; itrans_num; i++) { + // Verify that the transition list is sorted. + // Actually, we removed binary search so it no longer matters. + if (i>0 && (p_state->transitions[i].tran_char <= p_state->transitions[i-1].tran_char)) { + thinnfa_debug_critical(( + "%s: Transitions from state %s not sorted - %02x after %02x\n", + rname, + state_name(state_data), + p_state->transitions[i].tran_char, + p_state->transitions[i-1].tran_char + )); + ret = KISS_ERROR; + } + + // Verify that the transition points to a valid offset + if (thin_nfa_validation_add_transition(state_data, p_state->transitions[i].tran_char, + kiss_bnfa_offset_decompress(p_state->transitions[i].next_state_offset), + TRANS_DIRECTION_FORWARD) != KISS_OK) { + ret = KISS_ERROR; + } + } + + break; + } + + case KISS_BNFA_STATE_FULL: { + // Full state + u_int i; + + // Go over the transitions to all characters + for (i=0; ifull.transitions[i]), TRANS_DIRECTION_ANY) != KISS_OK) { + ret = KISS_ERROR; + } + } + break; + } + + case KISS_BNFA_STATE_MATCH: + // Add an implicit transition to the next state + if (thin_nfa_validation_add_transition(state_data, 0, + validation_state_epsilon_trans(state_data), TRANS_DIRECTION_FORWARD) != KISS_OK) { + ret = KISS_ERROR; + } + break; + + default: + // Can't really happen - checked already in thin_nfa_validation_find_states. + thinnfa_debug_critical(( + "%s: State %s has invalid type %d\n", + rname, + state_name(state_data), + state_data->type + )); + ret = KISS_ERROR; + break; + } + } + + return ret; +} + + +// See if there are states in the BNFA which were never visited +kiss_ret_val +thin_nfa_validation_unvisited_states(struct thinnfa_validation_status_s *validation) +{ + static const char rname[] = "thin_nfa_validation_unvisited_states"; + u_int i; + kiss_ret_val ret = KISS_OK; + + for (i=0; istate_num; i++) { + struct state_validation_data_s *state_data = &validation->state_data[i]; + if (state_data->level < 0) { + thinnfa_debug_critical(("%s: State %s never visited\n", rname, state_name(state_data))); + ret = KISS_ERROR; + } + } + return ret; +} + + +// Verify that pattern arrays buffer is self-consistant, and insert offsets into hash +static kiss_ret_val +thin_nfa_validation_check_pattern_arrays(struct thinnfa_validation_status_s *validation) +{ + static const char rname[] = "thin_nfa_validation_check_pattern_arrays"; + u_int pat_arr_offset; + + if (!validation->nfa->pattern_arrays || !validation->nfa->pattern_arrays_size) { + thinnfa_debug_critical(("%s: NULL pattern array (%p) or 0 length (%u)\n", rname, + validation->nfa->pattern_arrays, validation->nfa->pattern_arrays_size)); + return KISS_ERROR; + } + + pat_arr_offset = 0; + while (pat_arr_offset < validation->nfa->pattern_arrays_size) { + const kiss_thin_nfa_pattern_array_t *pat_arr; + if (!hash_insert(validation->pat_array_offset_ref_count, (void *)(intptr_t)pat_arr_offset, (void *)0)) { + thinnfa_debug_critical(("%s: failed to insert value into hash\n", rname)); + return KISS_ERROR; + } + pat_arr = kiss_thin_nfa_offset_to_pat_array_ptr(validation->nfa, pat_arr_offset); + if (pat_arr->n_patterns == 0) { + thinnfa_debug_critical(( + "%s: encounterd a pat array with 0 pattern at offset %u\n", + rname, + pat_arr_offset + )); + return KISS_ERROR; + } + pat_arr_offset += kiss_thin_nfa_pattern_array_size(pat_arr->n_patterns); + } + + if (pat_arr_offset != validation->nfa->pattern_arrays_size) { + thinnfa_debug_critical(("%s: pat_arr_offset (%u) is past total size (%u)\n", rname, + pat_arr_offset, validation->nfa->pattern_arrays_size)); + return KISS_ERROR; + } + + return KISS_OK; +} + + +// Verify that all match states point to a valid offset and increase ref count +static kiss_ret_val +thin_nfa_validation_check_match_states(const struct thinnfa_validation_status_s *validation) +{ + static const char rname[] = "thin_nfa_validation_check_match_states"; + kiss_ret_val ret = KISS_OK; + u_int i; + + for (i=0; i < validation->state_num; i++) { + struct state_validation_data_s *state_data = &validation->state_data[i]; + const kiss_bnfa_state_t *state; + u_int pat_arr_offset; + struct hashent **he; + + if (state_data->type != KISS_BNFA_STATE_MATCH) continue; + + state = kiss_bnfa_offset_to_state(validation->nfa->bnfa, state_data->bnfa_offset); + pat_arr_offset = state->match.match_id; + thinnfa_debug_extended(("%s: Found matching state %s pattern offset %u\n", rname, + state_name(state_data), pat_arr_offset)); + + he = hash_find_hashent(validation->pat_array_offset_ref_count, (void *)(intptr_t)pat_arr_offset); + if (he && *he) { + u_int *ref_count = (u_int *)(&((*he)->val)); + (*ref_count)++; + } else { + thinnfa_debug_critical(("%s: pattern offset (%u) for state %s is not valid!\n", rname, + pat_arr_offset, state_name(state_data))); + ret = KISS_ERROR; + } + } + + return ret; +} + +// Check that all offsets are used at least once. +static kiss_ret_val +thin_nfa_validation_unused_pat_offsets(const struct thinnfa_validation_status_s *validation) +{ + static const char rname[] = "thin_nfa_validation_unused_offsets"; + hash_iterator hi; + kiss_ret_val ret = KISS_OK; + + hi = hash_iterator_create(validation->pat_array_offset_ref_count); + if (!hi) { + thinnfa_debug_critical(("%s: failed to create hash iterator\n", rname)); + return KISS_ERROR; + } + + do { + struct hashent* he = hash_iterator_get_hashent(hi); + if (!he) { + thinnfa_debug_critical(("%s: failed to get hash entry\n", rname)); + ret = KISS_ERROR; + continue; + } + if ((u_int *)he->val == 0) { + thinnfa_debug_critical(("%s: offset %p has 0 ref count\n", rname, (u_int *)he->key)); + ret = KISS_ERROR; + } + // We use hash_iterator_next_ent and not hash_iterator_next becuase we store int as value + // and if the value is 0, hash_iterator_next will indidate that the iteration is over. + } while (hash_iterator_next_ent(hi)); + + hash_iterator_destroy(hi); + + return ret; +} + +// Check that the state map is correct +static kiss_ret_val +thin_nfa_validation_depth_map(const struct thinnfa_validation_status_s *validation) +{ + static const char rname[] = "thin_nfa_validation_depth_map"; + u_int i; + kiss_ret_val ret = KISS_OK; + + for (i=0; i < validation->state_num; i++) { + struct state_validation_data_s *state_data = &validation->state_data[i]; + u_int map_depth = kiss_bnfa_offset_to_depth( + validation->nfa, + kiss_bnfa_offset_compress(state_data->bnfa_offset) + ); + u_int validation_depth = state_data->level; + + if (state_data->flags & VALIDATION_STATE_IS_ANCHORED) { + // Validation treats ^ROOT as level 1 (and its children as level 1 more than real). + validation_depth--; + } + + if (validation_depth == map_depth) continue; + if (map_depth == validation->nfa->max_pat_len && validation_depth >= KISS_THIN_NFA_MAX_ENCODABLE_DEPTH) { + // kiss_bnfa_offset_to_depth returns max_pat_len for level 255 and up. + continue; + } + + thinnfa_debug_critical(("%s: State %s found in depth %d, map says %d (flags %x)\n", rname, + state_name(state_data), validation_depth, map_depth, state_data->flags)); + ret = KISS_ERROR; + } + + return ret; +} + + +static void +thin_nfa_validation_fini(struct thinnfa_validation_status_s *validation) +{ + static const char rname[] = "thin_nfa_validation_fini"; + if (validation->state_data != NULL) { + fw_kfree(validation->state_data, validation->state_num * sizeof(struct state_validation_data_s), rname); + validation->state_data = NULL; + } + if (validation->offset_to_data != NULL) { + hash_destroy(validation->offset_to_data); + validation->offset_to_data = NULL; + } + if (validation->pat_array_offset_ref_count != NULL) { + hash_destroy(validation->pat_array_offset_ref_count); + validation->pat_array_offset_ref_count = NULL; + } + + validation->nfa = NULL; +} + + +static kiss_ret_val +thin_nfa_validation_init(const KissThinNFA *nfa, struct thinnfa_validation_status_s *validation) +{ + static const char rname[] = "thin_nfa_validation_init"; + + bzero(validation, sizeof(*validation)); + validation->nfa = nfa; + + if (thin_nfa_validation_count_states(validation) < 0) { + thinnfa_debug_err(("%s: Failed to count states\n", rname)); + goto failure; + } + + // Allocate data for state validation information + validation->state_data = (struct state_validation_data_s *)fw_kmalloc_sleep( + validation->state_num * sizeof(struct state_validation_data_s), + rname + ); + if (!validation->state_data) { + thinnfa_debug_err(("%s: Failed to allocate %d state pointers\n", rname, validation->state_num)); + goto failure; + } + + // Allocate BNFA offset -> validation data mapping + validation->offset_to_data = hash_intcreate_ex(validation->state_num, FW_KMEM_SLEEP); + if (!validation->offset_to_data) { + thinnfa_debug_err(( + "%s: Failed to allocate hash table for validating %d states\n", + rname, + validation->state_num + )); + goto failure; + } + + // Allocate pattern arrays offset -> ref count mapping + validation->pat_array_offset_ref_count = hash_intcreate_ex(nfa->match_state_num, FW_KMEM_SLEEP); + if (!validation->pat_array_offset_ref_count) { + thinnfa_debug_err(( + "%s: Failed to allocate hash table for offsets - %u finite states\n", + rname, + nfa->match_state_num + )); + goto failure; + } + + if (thin_nfa_validation_find_states(validation) != KISS_OK) { + thinnfa_debug_err(("%s: Failed to fill NFA state info\n", rname)); + goto failure; + } + + return KISS_OK; + +failure: + thin_nfa_validation_fini(validation); + return KISS_ERROR; +} + + +BOOL +kiss_thin_nfa_is_valid(const KissThinNFA *nfa_h) +{ + static const char rname[] = "kiss_thin_nfa_is_valid"; + BOOL valid = FALSE; + struct thinnfa_validation_status_s validation; + + // Allocate and initialize validation data + if (thin_nfa_validation_init(nfa_h, &validation) != KISS_OK) { + thinnfa_debug_err(("%s: Failed to initialize validation data\n", rname)); + + // We can't validate, so we assume the Thin NFA is valid. + valid = TRUE; + goto finish; + } + + // Do a BFS scan to verify relations, + // see that all states are reached, verify that pattern offsets are used correctly. + if (thin_nfa_validation_scan_tree(&validation) != KISS_OK) goto finish; + if (thin_nfa_validation_unvisited_states(&validation) != KISS_OK) goto finish; + if (thin_nfa_validation_check_pattern_arrays(&validation) != KISS_OK) goto finish; + if (thin_nfa_validation_check_match_states(&validation) != KISS_OK) goto finish; + if (thin_nfa_validation_unused_pat_offsets(&validation) != KISS_OK) goto finish; + if (thin_nfa_validation_depth_map(&validation) != KISS_OK) goto finish; + + valid = TRUE; + +finish: + if (valid) { + thinnfa_debug_major(("%s: Thin NFA %p validation succeeded\n", rname, nfa_h)); + } else { + thinnfa_debug_critical(("%s: Thin NFA %p validation failed\n", rname, nfa_h)); + } + thin_nfa_validation_fini(&validation); + return valid; +} + + +// Thin NFA Dump code: +// From here, till the end of the file, the code is about dumping the automaton in different formats. +// CSV dump - For Excel +// XML dump - For the JFlap automaton visualisation applet. +// Wiki dump - For the Wiki {graph-from-table} plugin. +static int *xml_dump_level_positions; +static u_int xml_dump_level_positions_size; + +static void +xml_dump_positions_init(const struct thinnfa_validation_status_s *validation) +{ + static const char rname[] = "xml_dump_positions_init"; + u_int i; + + // Allocate a level->position map + xml_dump_level_positions_size = validation->nfa->max_pat_len; + xml_dump_level_positions = (int *)fw_kmalloc( + xml_dump_level_positions_size * sizeof(*xml_dump_level_positions), + rname + ); + if (!xml_dump_level_positions) { + thinnfa_debug_critical(( + "%s: Failed to allocate positions array (%d entries)\n", + rname, + xml_dump_level_positions_size + )); + // All X positions will be 0. + return; + } + + for (i=0; i= xml_dump_level_positions_size) { + *y = 0; + } else { + *y = xml_dump_level_positions[level]; + xml_dump_level_positions[level] += 100; + } + *x = level*100; +} + + +static void +xml_dump_print_header(const struct thinnfa_validation_status_s *validation) +{ + xml_dump_positions_init(validation); + + kdprintf("\n"); + kdprintf(" "); + kdprintf("\n"); + kdprintf("\tfa\n"); + kdprintf("\t\n"); + kdprintf("\t\t\n"); +} + + +static void +xml_dump_print_transition_ex(const struct state_validation_data_s *from_state, + u_char tran_char, kiss_bnfa_offset_t next_state_off, BOOL is_epsilon) +{ + kdprintf("\t\t" "\n"); + kdprintf("\t\t\t" "%d\n", from_state->bnfa_offset); + kdprintf("\t\t\t" "%d\n", next_state_off); + if (is_epsilon) { + kdprintf("\t\t\t" "\n"); // Epsilon + } else { + kdprintf("\t\t\t" "%s\n", char_to_printable(tran_char)); + } + kdprintf("\t\t" "\n"); +} + + +static void +xml_dump_print_state_start(const struct state_validation_data_s *state_data) +{ + const KissThinNFA *nfa = state_data->validation->nfa; + int x, y; + kiss_bnfa_offset_t epslion_trans; + + xml_dump_get_position(state_data->level, &x, &y); + + kdprintf("\t\t" "\n", + state_data->bnfa_offset, state_name(state_data)); + kdprintf("\t\t\t" "%d\n", x); + kdprintf("\t\t\t" "%d\n", y); + if (state_data->bnfa_offset == nfa->min_bnfa_offset) { + kdprintf("\t\t\t" "\n"); + } + if (state_data->type == KISS_BNFA_STATE_MATCH) { + kdprintf("\t\t\t" "\n"); + } + kdprintf("\t\t" "\n"); + + // Print an epsilon transition, if there is one + epslion_trans = validation_state_epsilon_trans(state_data); + if (epslion_trans != KISS_BNFA_OFFSET_INVALID) { + xml_dump_print_transition_ex(state_data, '\0', epslion_trans, TRUE); + } +} + + +static void +xml_dump_print_transition(const struct state_validation_data_s *from_state, + u_char tran_char, kiss_bnfa_offset_t next_state_off) +{ + xml_dump_print_transition_ex(from_state, tran_char, next_state_off, FALSE); +} + + +static void +xml_dump_print_state_end(CP_MAYBE_UNUSED const struct state_validation_data_s *state_data) +{ + // Nothing to do +} + +static void +xml_dump_print_trailer(CP_MAYBE_UNUSED const struct thinnfa_validation_status_s *validation) +{ + kdprintf("\t\n"); + kdprintf("\n"); + + xml_dump_positions_fini(); +} + +static thin_nfa_dump_cbs_t xml_dump_cbs = { + xml_dump_print_header, + xml_dump_print_state_start, + xml_dump_print_transition, + xml_dump_print_state_end, + xml_dump_print_trailer, + THIN_NFA_DUMP_SKIP_ROOT_TRANS +}; + +static void +wiki_dump_print_header(CP_MAYBE_UNUSED const struct thinnfa_validation_status_s *validation) +{ + // Start generating state names suitable for Wiki + doing_wiki_dump = 1; + + // The graph-from-table plugin will display the table lines below as a graph + kdprintf("{graph-from-table}\n"); +} + +static const char * +wiki_dump_state_color(const struct state_validation_data_s *state_data) +{ + if (state_data->bnfa_offset==state_data->validation->nfa->min_bnfa_offset) return "cyan"; // Initial + switch (state_data->type) { + case KISS_BNFA_STATE_FULL: return "yellow"; + case KISS_BNFA_STATE_PARTIAL: return "white"; + case KISS_BNFA_STATE_MATCH: return "green"; + + case KISS_BNFA_STATE_TYPE_NUM: break; + } + return "red"; // Shouldn't happen +} + +static void +wiki_dump_print_state(const struct state_validation_data_s *state_data) +{ + kiss_bnfa_offset_t epsilon_trans; + + // Format: |from|to|trans attrs|from attrs|to attrs| + // to, trans attrs, to attrs are omitted, so we only provide the state's attributes + kdprintf("|%d| | |label=\"%s\",fillcolor=%s|\n", + state_data->bnfa_offset, + state_name(state_data), + wiki_dump_state_color(state_data)); + + // Print epsilon transition, if any + epsilon_trans = validation_state_epsilon_trans(state_data); + if (epsilon_trans != KISS_BNFA_OFFSET_INVALID) { + // Format: |from|to|trans attrs| + kdprintf("|%d|%d|color=red|\n", state_data->bnfa_offset, epsilon_trans); + } +} + + +static void +wiki_dump_print_transition(const struct state_validation_data_s *from_state, + u_char tran_char, kiss_bnfa_offset_t next_state_off) +{ + // Format: |from|to|trans attrs| + kdprintf("|%d|%d|label=\"%s\"|\n", from_state->bnfa_offset, next_state_off, char_to_printable(tran_char)); +} + + +static void +wiki_dump_print_state_end(CP_MAYBE_UNUSED const struct state_validation_data_s *state_data) +{ + // Nothing to do +} + + +static void +wiki_dump_print_trailer(CP_MAYBE_UNUSED const struct thinnfa_validation_status_s *validation) +{ + kdprintf("{graph-from-table}\n"); + doing_wiki_dump = 0; +} + + +static thin_nfa_dump_cbs_t wiki_dump_cbs = { + wiki_dump_print_header, + wiki_dump_print_state, + wiki_dump_print_transition, + wiki_dump_print_state_end, + wiki_dump_print_trailer, + THIN_NFA_DUMP_SKIP_ROOT_TRANS +}; + + +#ifdef KERNEL +#define kdprintf_no_prefix kdprintf +#endif + +static void +csv_dump_print_header(CP_MAYBE_UNUSED const struct thinnfa_validation_status_s *validation) +{ + u_int i; + + // Start generating state names suitable for CSV / Excel + doing_csv_dump = 1; + + // The graph-from-table plugin will display the table lines below as a graph + kdprintf("Tier 1 CSV Dump start\n"); + + kdprintf_no_prefix( + "state_offset,state_name,level,is_match,is_partial,num_of_transitions,match_id_offset,fail_state_offset" + ); + for (i = 0; i < KISS_PM_ALPHABET_SIZE; i++) { + u_char ch = (u_char)i; + kdprintf_no_prefix(","); + switch (ch) { + // Some printable characters are problamtic in CSV files + case '\\': kdprintf_no_prefix("bslash"); break; + case ',': kdprintf_no_prefix("comma"); break; + case '\'': kdprintf_no_prefix("quote"); break; + case '\"': kdprintf_no_prefix("dquote"); break; + case ' ': kdprintf_no_prefix("space"); break; + default: + if (isprint(ch)) { + kdprintf_no_prefix("%c", ch); + } else { + kdprintf_no_prefix("0x%02X", ch); + } + break; + } + } + kdprintf_no_prefix("\n"); +} + + +// Used to detect characters without a transition +static u_int csv_dump_next_trans; + +static void +csv_dump_print_state_start(const struct state_validation_data_s *state_data) +{ + const kiss_bnfa_state_t *bnfa = state_data->validation->nfa->bnfa; + const kiss_bnfa_state_t *state = kiss_bnfa_offset_to_state(bnfa, state_data->bnfa_offset); + kiss_bnfa_offset_t epsilon_trans = validation_state_epsilon_trans(state_data); + + // Basic data - state_offset,state_name,level,is_match,is_partial, + // num_of_transitions,match_id_offset,fail_state_offset + kdprintf_no_prefix("%d,%s,%d,%u,%u,%u", + state_data->bnfa_offset, + state_name(state_data), + state_data->level, + (state_data->type==KISS_BNFA_STATE_MATCH), + (state_data->type==KISS_BNFA_STATE_PARTIAL), + validation_state_trans_num(state_data) + ); + if (state_data->type == KISS_BNFA_STATE_MATCH) { + kdprintf_no_prefix(",%d", state->match.match_id); + } else { + kdprintf_no_prefix(", "); + } + if (epsilon_trans != KISS_BNFA_OFFSET_INVALID) { + kdprintf_no_prefix(",%d", epsilon_trans); + } else { + kdprintf_no_prefix(", "); + } + + csv_dump_next_trans = '\0'; +} + + +static void +csv_dump_print_transition(CP_MAYBE_UNUSED const struct state_validation_data_s *from_state, + u_char tran_char, kiss_bnfa_offset_t next_state_off) +{ + // Print skipped characters + while (csv_dump_next_trans < tran_char) { + kdprintf_no_prefix(", "); + csv_dump_next_trans++; + } + + kdprintf_no_prefix(",%d", next_state_off); + csv_dump_next_trans = tran_char + 1; +} + + +static void +csv_dump_print_state_end(CP_MAYBE_UNUSED const struct state_validation_data_s *state_data) +{ + // Print skipped characters at the tail + while (csv_dump_next_trans < KISS_PM_ALPHABET_SIZE) { + kdprintf_no_prefix(", "); + csv_dump_next_trans++; + } + + kdprintf_no_prefix("\n"); +} + + +static void +csv_dump_print_trailer(CP_MAYBE_UNUSED const struct thinnfa_validation_status_s *validation) +{ + kdprintf("Tier 1 CSV Dump end\n"); + doing_csv_dump = 0; +} + + +static thin_nfa_dump_cbs_t csv_dump_cbs = { + csv_dump_print_header, + csv_dump_print_state_start, + csv_dump_print_transition, + csv_dump_print_state_end, + csv_dump_print_trailer, + THIN_NFA_DUMP_FLAGS_NONE +}; + + +static void +thin_nfa_dump_state( + const struct thinnfa_validation_status_s *validation, + const struct state_validation_data_s *state_data, + const thin_nfa_dump_cbs_t *dump_format_cbs +) +{ + static const char rname[] = "thin_nfa_dump_state"; + const kiss_bnfa_state_t *state = kiss_bnfa_offset_to_state(validation->nfa->bnfa, state_data->bnfa_offset); + const kiss_bnfa_offset_t root_offset = validation->root->bnfa_offset; + u_int i, trans_num; + + // Print some stuff at the state start + dump_format_cbs->state_start_cb(state_data); + + // Print the transition table + trans_num = validation_state_trans_num(state_data); + for (i = 0; i < trans_num; i++) { + u_char tran_char; + kiss_bnfa_offset_t tran_bnfa_offset; + + // Get the transition's character and next state + switch (state_data->type) { + case KISS_BNFA_STATE_PARTIAL: + tran_char = state->partial.transitions[i].tran_char; + tran_bnfa_offset = kiss_bnfa_offset_decompress(state->partial.transitions[i].next_state_offset); + break; + + case KISS_BNFA_STATE_FULL: + tran_char = (u_char)i; + tran_bnfa_offset = kiss_bnfa_offset_decompress(state->full.transitions[i]); + break; + + default: + // KISS_BNFA_STATE_MATCH has no transitions + thinnfa_debug_critical(("%s: Bad type %d\n", rname, state_data->type)); + return; + } + + // Possibly skip root transitions + if ((tran_bnfa_offset==root_offset) && (dump_format_cbs->flags & THIN_NFA_DUMP_SKIP_ROOT_TRANS)) continue; + + // Print the transition + dump_format_cbs->transition_cb(state_data, tran_char, tran_bnfa_offset); + } + + // Print some stuff at the state end + dump_format_cbs->state_end_cb(state_data); +} + +static kiss_ret_val +thin_nfa_dump(const KissThinNFA *nfa, const thin_nfa_dump_cbs_t *dump_format_cbs) +{ + static const char rname[] = "thin_nfa_dump"; + struct thinnfa_validation_status_s validation; + u_int i; + kiss_ret_val ret = KISS_ERROR; + + // We don't want to crash or loop if the Thin NFA is corrupt, so validate first + if (thin_nfa_validation_init(nfa, &validation) != KISS_OK) { + thinnfa_debug_critical(("%s: Failed to initialize validation data\n", rname)); + goto cleanup; + } + + // Go over the tree and follow all transitions + if (thin_nfa_validation_scan_tree(&validation) != KISS_OK) { + thinnfa_debug_critical(("%s: Tree scan failed - the BNFA is corrupt\n", rname)); + // Continue despite failure. We'll end up with ugly state names. + } + + // The graph-from-table plugin will display the table lines below as a graph + dump_format_cbs->start_cb(&validation); + + // Go over states and print them + for (i=0; iend_cb(&validation); + + ret = KISS_OK; +cleanup: + thin_nfa_validation_fini(&validation); + return ret; +} + + +kiss_ret_val +kiss_thin_nfa_dump(const KissThinNFA *nfa, enum kiss_pm_dump_format_e format) +{ + static const char rname[] = "kiss_thin_nfa_dump"; + thin_nfa_dump_cbs_t *format_cbs = NULL; + + switch (format) { + case KISS_PM_DUMP_XML: + format_cbs = &xml_dump_cbs; + break; + case KISS_PM_DUMP_CSV: + format_cbs = &csv_dump_cbs; + break; + case KISS_PM_DUMP_WIKI: + format_cbs = &wiki_dump_cbs; + break; + } + + if (!format_cbs) { + thinnfa_debug_critical(("%s: Invalid dump format %d\n", rname, format)); + return KISS_ERROR; + } + + return thin_nfa_dump(nfa, format_cbs); +} +SASAL_END diff --git a/components/utils/pm/kiss_thin_nfa_base.h b/components/utils/pm/kiss_thin_nfa_base.h new file mode 100644 index 0000000..9a1830d --- /dev/null +++ b/components/utils/pm/kiss_thin_nfa_base.h @@ -0,0 +1,261 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __kiss_thin_nfa_base_h__ +#define __kiss_thin_nfa_base_h__ + +#include "general_adaptor.h" + +// ****************************** OVERVIEW ******************************* +// Contians basic Thin NFA structure, used by kiss_pm and bolt (prescan) +// *********************************************************************** + +#define KISS_THIN_NFA_ALPHABET_SIZE 256 + +// Binary representation of the Thin NFA. +// This is what's actually used during runtime. +// +// Offsets in the BNFA +// ------------------- +// Offsets are signed 32-bit integers, specifying the distance in bytes from the "offset 0" point. +// +// Offset 0 isn't the BNFA start - there are negative offsets: +// All full states are in negative offsets. This is the only way to know that a state is full. +// All other states are in positive offsets. +// +// In full states, offsets are encoded in 16 bits. +// In partial states, offsets are encoded in 24 bits. +// Offsets are compressed: +// Positive offsets are divided by 4. This is possible because all state sizes are a multiple of 4 bytes. +// Negative offsets are divided by 512 (the size of a full state). This is possible because negative offsets +// are only used for full states, so their offsets are a (negative) multiple of the state size. +// +// Structure of a BNFA state +// ------------------------- +// 1. Full state: +// a. No header. Identified by the fact that its BNFA offset is negative. +// b. 256 transitions, 16bits each (uncompressed offsets). +// 2. Common header, to partial and match states: +// a. State type - 2 bits. +// 3. Partial state: +// a. State type - 2 bits. +// b. Transition number - 6 bits. +// c. Fail state offset (compresed) - 24 bits. +// d. Per transition: +// 1) Character - 8 bits +// 2) Next state offset (compressed) - 24 bits +// 4. Match state: +// a. State type - 2 bits. +// b. Unused - 6 bits. +// c. Match ID - 24 bits. +// +// Examples: +// +// Partial state, 2 transitions - 'a'->100, 'b'->104, fail-> -3072 +// +----+---+-----+---+-----+---+-----+ +// Bits: | 2 | 6 | 24 | 8 | 24 | 8 | 24 | +// +----+---+-----+---+-----+---+-----+ +// Data: | P | 2 | -3 | a | 25 | b | 26 | +// +----+---+-----+---+-----+---+-----+ +// +// Full state, 0x00->200, 0x01->204, 0xff->280 +// +-----+-----+ +-----+ +// Bits: | 16 | 16 | | 16 | +// +-----+-----+ .... +-----+ +// Data: | 50 | 51 | | 70 | +// +-----+-----+ +-----+ + + +// Types for normal and compressed (see comment above) BNFA offsets + +typedef int kiss_bnfa_offset_t; // Offset in bytes +typedef int kiss_bnfa_comp_offset_t; // Compressed offset +typedef short kiss_bnfa_short_offset_t; // Compressed offset in 16bits (for full states) + +#define KISS_BNFA_OFFSET_INVALID ((int)0x80000000) + +// State types +typedef enum { + KISS_BNFA_STATE_PARTIAL, + KISS_BNFA_STATE_MATCH, + KISS_BNFA_STATE_FULL, + + KISS_BNFA_STATE_TYPE_NUM +} kiss_bnfa_state_type_t; + + +// State structure + +// Use some header bits for the state type +#define KISS_BNFA_STATE_TYPE_BITS 2 + +// The type must fit in KISS_BNFA_STATE_TYPE_BITS bits +KISS_ASSERT_COMPILE_TIME(KISS_BNFA_STATE_TYPE_NUM <= (1<common.type; +} + + +// State size + +// Get the size of a partial state with N transitions +static CP_INLINE u_int +kiss_bnfa_partial_state_size(u_int trans_num) +{ + // Header + transition table + return KISS_OFFSETOF(kiss_bnfa_partial_state_t, transitions) + + sizeof(struct kiss_bnfa_partial_transition_s) * (trans_num); +} + +// Get the size of an existing state +static CP_INLINE u_int +kiss_bnfa_state_size(const kiss_bnfa_state_t *bnfa, kiss_bnfa_offset_t offset) +{ + switch (kiss_bnfa_state_type(bnfa, kiss_bnfa_offset_compress(offset))) { + case KISS_BNFA_STATE_PARTIAL: { + const kiss_bnfa_state_t *state = kiss_bnfa_offset_to_state(bnfa, offset); + return kiss_bnfa_partial_state_size(state->partial.trans_num); + } + case KISS_BNFA_STATE_MATCH: return sizeof(kiss_bnfa_match_state_t); + case KISS_BNFA_STATE_FULL: return sizeof(kiss_bnfa_full_state_t); + + case KISS_BNFA_STATE_TYPE_NUM: break; // Can't happen + } + + return 0; +} + +// Flags for kiss_thin_nfa_s.flags and kiss_thin_nfa_prescan_hdr_s.flags +enum kiss_thin_nfa_flags_e { + KISS_THIN_NFA_USE_CHAR_XLATION = 0x01, // Used for caseless and/or digitless + KISS_THIN_NFA_HAS_ANCHOR = 0x02, // State at offset 0 is anchored root, not root +}; + + +#endif // __kiss_thin_nfa_base_h__ diff --git a/components/utils/pm/kiss_thin_nfa_build.cc b/components/utils/pm/kiss_thin_nfa_build.cc new file mode 100644 index 0000000..7dcbecb --- /dev/null +++ b/components/utils/pm/kiss_thin_nfa_build.cc @@ -0,0 +1,242 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Thin NFA Construction and Destruction +// ------------------------------------- +// This file contains code that builds a Thin NFA. +// The functions here may be called from compilation, serialization and de-serialization contexts. +// The code allows allocating and releasing the Thin NFA structure, as well as serializing and deserializing it. + +#include "kiss_thin_nfa_impl.h" +#include "sasal.h" + +SASAL_START // Multiple Pattern Matcher +// Allocate and fill in a pattern ID structure +kiss_ret_val +kiss_thin_nfa_add_pattern_id(kiss_thin_nfa_pattern_list_t **pat_list_p, const kiss_thin_nfa_pattern_t *new_pat) +{ + static const char rname[] = "kiss_thin_nfa_add_pattern_id"; + kiss_thin_nfa_pattern_list_t **pat_ptr; + kiss_thin_nfa_pattern_list_t *pat; + + // Go over the pattern list - look for our pattern, and find the end + for (pat_ptr = pat_list_p; *pat_ptr != NULL; pat_ptr = &((*pat_ptr)->next)) { + kiss_thin_nfa_pattern_t *list_pat = &(*pat_ptr)->pattern; + + if (list_pat->id == new_pat->id) { + // Already there - nothing to do + thinnfa_debug(( + "%s: Pattern already exists - ID=%d flags=%x(%x) len=%d(%d)\n", + rname, + new_pat->id, + new_pat->pattern_id_flags, + list_pat->pattern_id_flags, + new_pat->len, + list_pat->len + )); + return KISS_OK; + } + } + + // Allocate the pattern structure + pat = (kiss_thin_nfa_pattern_list_t *)kiss_pmglob_memory_kmalloc(sizeof(kiss_thin_nfa_pattern_list_t), rname); + if (!pat) { + thinnfa_debug_err(("%s: Failed to allocate pattern id\n", rname)); + return KISS_ERROR; + } + + // Fill in the fields + bcopy(new_pat, &pat->pattern, sizeof(pat->pattern)); + + thinnfa_debug(( + "%s: Added pattern ID=%d flags=%x len=%d\n", + rname, + new_pat->id, + new_pat->pattern_id_flags, + new_pat->len + )); + + // Add to the linked list of patternss. + *pat_ptr = pat; + pat->next = NULL; + + return KISS_OK; +} + + +// Free an entire list of pattern IDs. +void +kiss_thin_nfa_free_pattern_ids(kiss_thin_nfa_pattern_list_t *pat_list) +{ + static const char rname[] = "kiss_thin_nfa_free_pattern_ids"; + kiss_thin_nfa_pattern_list_t *pat, *next; + + for (pat = pat_list; pat != NULL; pat = next) { + next = pat->next; + thinnfa_debug(( + "%s: Releasing pattern ID=%d flags=%x len=%u\n", + rname, + pat->pattern.id, + pat->pattern.pattern_id_flags, + pat->pattern.len + )); + kiss_pmglob_memory_kfree(pat, sizeof(kiss_thin_nfa_pattern_list_t), rname); + } + return; +} + + +// Allocate and initialize statistics +static kiss_ret_val +kiss_thin_nfa_stats_init(kiss_thin_nfa_stats stats) +{ + + if (kiss_pm_stats_common_init(&(stats->common)) != KISS_OK) { + return KISS_ERROR; + } + + bzero(&(stats->specific), sizeof(struct kiss_thin_nfa_specific_stats_s)); + + return KISS_OK; +} + + +// Free statistics +static void +kiss_thin_nfa_stats_free(kiss_thin_nfa_stats stats) +{ + kiss_pm_stats_common_free(&(stats->common)); +} + + +static kiss_ret_val +kiss_thin_nfa_alloc_depth_map(KissThinNFA *nfa) +{ + static const char rname[] = "kiss_thin_nfa_alloc_depth_map"; + kiss_bnfa_comp_offset_t min_comp_off, max_comp_off; + + // The depth map is addressed by the compressed offset + min_comp_off = kiss_bnfa_offset_compress(nfa->min_bnfa_offset); + max_comp_off = kiss_bnfa_offset_compress(nfa->max_bnfa_offset); + + nfa->depth_map.size = max_comp_off - min_comp_off; + nfa->depth_map.mem_start = (u_char *)kiss_pmglob_memory_kmalloc_ex(nfa->depth_map.size, rname, FW_KMEM_SLEEP); + if (!nfa->depth_map.mem_start) { + thinnfa_debug_err(( + "%s: Error allocating the depth map, size %d (BNFA offsets %d:%d)\n", + rname, + nfa->depth_map.size, + nfa->min_bnfa_offset, + nfa->max_bnfa_offset + )); + return KISS_ERROR; + } + // Find the place for offset 0. min_comp_offset is negative, so it's after mem_start. + nfa->depth_map.offset0 = nfa->depth_map.mem_start - min_comp_off; + + return KISS_OK; +} + + +static void +kiss_thin_nfa_destroy_depth_map(KissThinNFA *nfa) +{ + static const char rname[] = "kiss_thin_nfa_destroy_depth_map"; + if (nfa->depth_map.mem_start != NULL) { + kiss_pmglob_memory_kfree(nfa->depth_map.mem_start, nfa->depth_map.size, rname); + nfa->depth_map.mem_start = NULL; + nfa->depth_map.offset0 = NULL; + } +} + + +KissThinNFA::~KissThinNFA() +{ + static const char rname[] = "~KissThinNFA"; + // the code here was once in kiss_thin_nfa_destroy + u_int bnfa_size = max_bnfa_offset - min_bnfa_offset; + + thinnfa_debug_major(("%s: Destroying Thin NFA %p, bnfa size=%d\n", rname, + this, bnfa_size)); + + if(bnfa_start != NULL) { + kiss_pmglob_memory_kfree(bnfa_start, bnfa_size, rname); + bnfa_start = NULL; + bnfa = NULL; + } + + kiss_thin_nfa_stats_free(&stats); + + if (pattern_arrays != NULL) { + kiss_pmglob_memory_kfree(pattern_arrays, pattern_arrays_size, rname); + pattern_arrays = NULL; + } + + kiss_thin_nfa_destroy_depth_map(this); +} + + +// Allocate a Thin NFA. The match info array and BNFA are left empty. +std::unique_ptr +kiss_thin_nfa_create(u_int match_state_num, kiss_bnfa_offset_t min_offset, kiss_bnfa_offset_t max_offset) +{ + static const char rname[] = "kiss_thin_nfa_create"; + + // Allocate the structure + auto nfa = std::make_unique(); + void *nfa_ptr = nfa.get(); + bzero(nfa_ptr, sizeof(*nfa)); + nfa->min_bnfa_offset = min_offset; + nfa->max_bnfa_offset = max_offset; + nfa->match_state_num = match_state_num; + + // Allocate the bnfa array. Not initialized. + u_int bnfa_size = max_offset - min_offset; + nfa->bnfa_start = (kiss_bnfa_state_t *)kiss_pmglob_memory_kmalloc_ex(bnfa_size, rname, FW_KMEM_SLEEP); + if (!nfa->bnfa_start) { + thinnfa_debug_err(( + "%s: Error allocating the bnfa - size %d (offset %d:%d)\n", + rname, + bnfa_size, + min_offset, + max_offset + )); + return nullptr; + } + + // Calculate bnfa so bnfa_start would be at offset min_offset (min_offset<0, so bnfa>bnfa_start) + nfa->bnfa = (kiss_bnfa_state_t *)((char *)nfa->bnfa_start - min_offset); + + // Init the statistics + if (kiss_thin_nfa_stats_init(&(nfa->stats)) != KISS_OK) { + thinnfa_debug_err(("%s: Error initializing statistics structure\n", rname)); + return nullptr; + } + + // Allocate the state depth map + if (kiss_thin_nfa_alloc_depth_map(nfa.get()) != KISS_OK) { + return nullptr; + } + + thinnfa_debug_major(( + "%s: Allocated Thin NFA %p, bnfa size=%d (offsets %d:%d)\n", + rname, + nfa.get(), + bnfa_size, + min_offset, + max_offset + )); + + return nfa; +} +SASAL_END diff --git a/components/utils/pm/kiss_thin_nfa_compile.cc b/components/utils/pm/kiss_thin_nfa_compile.cc new file mode 100644 index 0000000..4c0e968 --- /dev/null +++ b/components/utils/pm/kiss_thin_nfa_compile.cc @@ -0,0 +1,2232 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include +#include "pm_adaptor.h" +#include "kiss_hash.h" +#include "kiss_thin_nfa_impl.h" +#include "kiss_patterns.h" +#include "sasal.h" + +SASAL_START // Multiple Pattern Matcher +// Flag for a Thin NFA state +typedef enum { + THIN_NFA_STATE_FULL = 0x01, // We want a full state table for this state + THIN_NFA_STATE_MATCH = 0x02, // A matching state + THIN_NFA_STATE_ROOT = 0x04, // The root or anchored root state + THIN_NFA_STATE_MAX_IDENTICAL_CHAR = 0x08, // Maximal sequence of identical characters + THIN_NFA_STATE_ANCHORED = 0x10, // A part of the anchored tree + THIN_NFA_STATE_BUILT_TABLE = 0x20, // Already built the BNFA transition table + THIN_NFA_STATE_REACH_FROM_FULL = 0x40, // The state is reachable from full state +} nfa_thin_state_flags_t; + +// A Thin NFA state, or a node in the trie, during compilation time +struct kiss_thin_nfa_state_s { + u_int state_id; // Sequencial number, starting from 0 + nfa_thin_state_flags_t flags; + u_int depth; // Level in the trie + kiss_thin_nfa_pattern_list_t *ids; // For finite state, patterns associated with it + struct kiss_thin_nfa_state_s *bfs_q; // Use for a BFS iteration on the trie + struct thin_nfa_comp_s *comp; // Saves passing this pointer around + // Outgoing transitions + struct kiss_thin_nfa_state_s *child; // First child of this state + u_int num_trans; // Number of transitions + struct kiss_thin_nfa_state_s *fail_state; + // Incoming transition + struct kiss_thin_nfa_state_s *sibling; // Next child of this state's father + u_char tran_char; // The character that takes us to this state + // BNFA offset + kiss_bnfa_offset_t bnfa_offset; // Where the real state is + kiss_bnfa_offset_t bnfa_incoming_off; // Where incoming transitions should jump (possibly a match state) + // DEBUG ONLY + const u_char *pattern_text; // Points into the user's pattern list. Not null terminated +}; +typedef struct kiss_thin_nfa_state_s kiss_thin_nfa_state_t; + + +// Blocks to hold states . A pretty simple pool mechanism. +// Not very much needed. We currently use it to iterate states by ID order, and for state pointer validation. +#define MAX_THIN_NFA_STATES_BLOCKS 1000 +#define KISS_NFA_MAX_STATES_PER_BLOCK 1000 +#define KISS_NFA_MAX_STATES_BLOCK_SIZE (KISS_NFA_MAX_STATES_PER_BLOCK * sizeof(kiss_thin_nfa_state_t)) + + +// When do we want a full state? In the first X tiers (root included) and if more than Y transitions. +u_int kiss_thin_nfa_full_tiers_num_small = 2; // Old values, for PMs which must remain small +u_int kiss_thin_nfa_full_tiers_num_medium = 3; // Used for VSX / 32bit kernel, where memory is expensive +u_int kiss_thin_nfa_full_tiers_num = 7; // New value +u_int kiss_thin_nfa_max_partial_trans = 15; // Can't exceed KISS_BNFA_MAX_TRANS_NUM anyway +u_int kiss_thin_nfa_optimize_contig_chars = 1; + + +// Character translation table for caseless/digitless comparisons. +// +// The idea: +// Each character has a canonic character. This can be itself, or another. +// In a caseless Thin NFA, 'a' and '7' are canonic themselves, 'B' has canonic character 'b'. +// In a digitless Thin NFA, '7' is not canonic - its canonic character is '0'. +// Each character is also a member of a group, containing all characters with the same canonic character. +// In a caseless Thin NFA, 'a' and 'A' are in one group. +// In a digitless Thin NFA, all digits are in one group. +// Notice that a single Thin NFA can be caseless, digitless, neither or both. +// +// The data structure: +// tab - Translates each character into its canonic characer (possibly itself). +// rev - A linked list of characters belonging to the same group. The character itself +// is used instead of a pointer. The last character in the group points to the first. +// Example: For a caseless Thin NFA, rev['a']='A' and rev['A']='a'. +struct thin_nfa_char_trans_tab_s { + u_char tab[KISS_PM_ALPHABET_SIZE]; + u_char rev[KISS_PM_ALPHABET_SIZE]; +}; + + +// Flags for an entire Thin NFA during compilation +typedef enum { + THIN_NFA_FAIL_STATES_CALCULATED = 0x01, // Once we set this, we expect all states to have fail states. + THIN_NFA_ENABLE_ANCHOR_OPT = 0x02, // Enable optimization for anochored states + THIN_NFA_USE_RECURSIVE_COMPILE = 0x04, // Build full states recursively. Faster, unsuitable for kernel +} thin_nfa_comp_flags_e; + + +// A Thin NFA which is under construction. The compiled BNFA is constructed from this later. +struct thin_nfa_comp_s { + kiss_thin_nfa_state_t *root_state; // The root state (somewhere inside state_blocks) + kiss_thin_nfa_state_t *anchored_root_state; // The root for anchored patterns + u_int full_state_tier_num; // How many tiers would be full states? + u_int state_num; // How many states do we have so far + u_int match_state_num; // How many matching states do we have? + u_int full_state_num; // How many full states do we have? + KissPMError *error; // Error to be returned to the user. + thin_nfa_comp_flags_e flags; + struct thin_nfa_char_trans_tab_s *xlation_tab; // Caseless/digitless translation table + kiss_thin_nfa_state_t *state_blocks[MAX_THIN_NFA_STATES_BLOCKS]; // Dynamically allocated memory for states + std::unique_ptr runtime_nfa; // The final NFA we're building + kiss_hash_t patterns_hash; // Pattern array to offset mapping + kiss_bnfa_offset_t min_bnfa_off, max_bnfa_off; +}; + + +#if defined(DEBUG) +#define KISS_THIN_NFA_DO_VERIFICATIONS +#endif + + +#define MAX_STATE_NAME_LEN 100 +#define MAX_STATE_NAME_BUFS 4 + + +static kiss_thin_nfa_state_t * +kiss_thin_nfa_get_state_by_id(struct thin_nfa_comp_s *nfa_comp, u_int state_id, const char *caller) +{ + u_int block_index; + u_int index_in_block; + kiss_thin_nfa_state_t *block; + + // Find the block and the place in the block + block_index = state_id / KISS_NFA_MAX_STATES_PER_BLOCK; + index_in_block = state_id % KISS_NFA_MAX_STATES_PER_BLOCK; + if (block_index >= MAX_THIN_NFA_STATES_BLOCKS) { + thinnfa_debug_critical(("%s: State %d - invalid block index %d (max %d)\n", caller, + state_id, block_index, MAX_THIN_NFA_STATES_BLOCKS)); + return NULL; + } + block = nfa_comp->state_blocks[block_index]; + if (block == NULL) { + thinnfa_debug_critical(( + "%s: State %d - block index %d is not allocated yet\n", + caller, + state_id, + block_index + )); + return NULL; + } + + return &block[index_in_block]; +} + + +// DEBUG FUNCTION - return a printable name for the state, in a static buffer. +// Accepts a NULL state. +static const char * +state_name(const kiss_thin_nfa_state_t *state) +{ + static char buffers[MAX_STATE_NAME_BUFS][MAX_STATE_NAME_LEN]; + static u_int next_buf = 0; + u_int cur_buf; + char *name, *p; + + if (!state) { + // Happens when printing the root's fail state + return "NULL/-1"; + } + + // What's a state's name? + // Each state represents a prefix of one or more patterns. This prefix is the natural name for the state. + // We have the pattern text on the state. Its depth tells us how much of it do we need. + // We add the state ID as a suffix, to prevent ambiguituies (particularly for unprintable characters). + + //Choose a buffer to use. Allows calling several times under a single debug message. + cur_buf = next_buf; + if (cur_buf >= MAX_STATE_NAME_BUFS) cur_buf = 0; + next_buf = cur_buf + 1; + name = buffers[cur_buf]; + p = name; + + if (state->flags & THIN_NFA_STATE_ANCHORED) { + // Prefix for anchored states + *p = '^'; + p++; + } + + // Fill in the state name. Not null-terminated meanwhile. + if (state->pattern_text == NULL) { + const char *state_name; + // Only the root makes sense. But deal with a missing pattern text anyway + state_name = (state->flags & THIN_NFA_STATE_ROOT) ? "ROOT" : "INVALID"; + strcpy(p, state_name); + p += strlen(state_name); + } else { + u_int i; + // Normal state - use the relevant prefix of the pattern text + for (i=0; (idepth) && (ppattern_text[i]) ? state->pattern_text[i] : '.'; + p++; + } + } + + // Append the state ID. Removes ambituities (e.g. for unprintable characters) + snprintf(p, MAX_STATE_NAME_LEN-(p-name), "/%u", state->state_id); + name[MAX_STATE_NAME_LEN-1] = '\0'; + + return name; +} + + +#if defined(KISS_THIN_NFA_DO_VERIFICATIONS) + +// DEBUG FUNCTION - Verify that a state pointer points to a valid state +static BOOL +is_valid_state_ptr(struct thin_nfa_comp_s *nfa_comp, kiss_thin_nfa_state_t *state, const char *caller) +{ + kiss_thin_nfa_state_t *state_by_id; + + if (!state) { + thinnfa_debug_critical(("%s: Null state pointer\n", caller)); + return FALSE; + } + + state_by_id = kiss_thin_nfa_get_state_by_id(nfa_comp, state->state_id, caller); + if (!state_by_id) { + return FALSE; + } + + // Is the state where we expect it to be in the block? + if (state != state_by_id) { + thinnfa_debug_critical(("%s: State %p ID %d is invalid - should be at %p\n", caller, state, + state->state_id, state_by_id)); + return FALSE; + } + + return TRUE; +} + + +// DEBUG FUNCTION - Verify a state's transition table +static void +verify_state_ex(struct thin_nfa_comp_s *nfa_comp, kiss_thin_nfa_state_t *state, const char *caller) +{ + kiss_thin_nfa_state_t *child, *prev_child; + u_int actual_tran_num; + + // Is the pointer itself OK? + KISS_ASSERT(is_valid_state_ptr(nfa_comp, state, caller), + ("%s: Invalid state pointer %p\n", caller, state)); + + // Go over the transition table + actual_tran_num = 0; + prev_child = NULL; + for (child = state->child; child != NULL; child = child->sibling) { + // Valid pointer? + KISS_ASSERT(is_valid_state_ptr(nfa_comp, child, caller), + ("%s: State %s(%p) contains an invalid child %p after %02x\n", caller, state_name(state), state, + child, prev_child ? prev_child->tran_char : 0)); + + // Sorted in ascending order? + KISS_ASSERT(!prev_child || prev_child->tran_char < child->tran_char, + ("%s: State %s(%p) transition %02x -> %s after %02x -> %s\n", caller, + state_name(state), state, + child->tran_char, state_name(child), + prev_child->tran_char, state_name(prev_child))); + + actual_tran_num++; + if (actual_tran_num > state->num_trans) { + // We may be looping + break; + } + prev_child = child; + } + + // Counter matches list? + KISS_ASSERT(actual_tran_num == state->num_trans, + ("%s: State %s(%p) has %d transitions, but it should have %d\n", caller, state_name(state), state, + actual_tran_num, state->num_trans)); + + // Fail state? + if (nfa_comp->flags & THIN_NFA_FAIL_STATES_CALCULATED) { + if (state->fail_state == NULL) { + KISS_ASSERT(state == nfa_comp->root_state, ("%s: State %s has no fail state, but it is not root", + caller, state_name(state))); + } else { + KISS_ASSERT( + is_valid_state_ptr(nfa_comp, state->fail_state, caller), + "%s: State %s has an invalid fail state %p\n", + caller, + state_name(state), + state->fail_state + ); + } + } +} + + +// Use this for sanity test on a state +#define verify_state(nfa_comp, state) verify_state_ex(nfa_comp, state, FILE_LINE) + +#else // KISS_THIN_NFA_DO_VERIFICATIONS + +// Verifications disabled +#define verify_state(nfa_comp, state) + +#endif // KISS_THIN_NFA_DO_VERIFICATIONS + + +// Mark that a state needs to be full +static void +make_state_full(kiss_thin_nfa_state_t *state) +{ + if (state->flags & THIN_NFA_STATE_FULL) return; + ENUM_SET_FLAG(state->flags, THIN_NFA_STATE_FULL); + state->comp->full_state_num++; +} + + +// Mark that a state is matching +static void +make_state_matching(kiss_thin_nfa_state_t *state) +{ + if (state->flags & THIN_NFA_STATE_MATCH) return; + ENUM_SET_FLAG(state->flags, THIN_NFA_STATE_MATCH); + state->comp->match_state_num++; +} + + +// Allocate an empty state on an NFA. +// Initializes all fields to defaults. +static kiss_thin_nfa_state_t * +kiss_thin_nfa_state_create( + struct thin_nfa_comp_s *nfa_comp, + u_int depth, + const u_char *pattern_text, + nfa_thin_state_flags_t flags +) +{ + static const char rname[] = "kiss_thin_nfa_state_create"; + u_int state_id; + u_int block_index; + u_int index_in_block; + kiss_thin_nfa_state_t *block; + kiss_thin_nfa_state_t *state; + + // Find the next ID and the block it should be in + state_id = nfa_comp->state_num; + block_index = state_id / KISS_NFA_MAX_STATES_PER_BLOCK; + index_in_block = state_id % KISS_NFA_MAX_STATES_PER_BLOCK; + + thinnfa_debug_extended(("%s: Adding state %d depth %d\n", rname, state_id, depth)); + + // No more possible blocks? + if (block_index >= MAX_THIN_NFA_STATES_BLOCKS) { + thinnfa_debug_err(("%s: State %d in block %d exceeds the limit %d\n", rname, + state_id, block_index, MAX_THIN_NFA_STATES_BLOCKS)); + return NULL; + } + + // Allocate the block if needed (first state in the block) + block = nfa_comp->state_blocks[block_index]; + if (block == NULL) { + block = (kiss_thin_nfa_state_t *)fw_kmalloc_ex(KISS_NFA_MAX_STATES_BLOCK_SIZE, rname, FW_KMEM_SLEEP); + if (block == NULL) { + thinnfa_debug_err(("%s: Failed to allocate a state block size %lu for the state %u\n", rname, + KISS_NFA_MAX_STATES_BLOCK_SIZE, state_id)); + return NULL; + } + nfa_comp->state_blocks[block_index] = block; + } + + // Initialize the state + state = &block[index_in_block]; + + state->state_id = state_id; + state->flags = flags; + state->ids = NULL; + state->bfs_q = NULL; + state->child = NULL; + state->num_trans = 0; + state->fail_state = NULL; + state->sibling = NULL; + state->tran_char = '\0'; // Will be modified, except for the root + state->pattern_text = pattern_text; + state->depth = depth; + state->comp = nfa_comp; + state->bnfa_offset = KISS_BNFA_OFFSET_INVALID; + state->bnfa_incoming_off = KISS_BNFA_OFFSET_INVALID; + + // Do we want a full state? kiss_thin_nfa_full_tiers_num=2 means tiers 0 and 1, i.e. the root plus one, are full. + if (state->flags & THIN_NFA_STATE_ROOT) { + // The root must be full, because it has no fail state. + // The anchored root (if exists) is the first state, and must be full, for the bnfa_full_state_size + // condition to work. + make_state_full(state); + } else if (depth < nfa_comp->full_state_tier_num && !(state->flags & THIN_NFA_STATE_ANCHORED)) { + make_state_full(state); + } + + // Advance the counter + nfa_comp->state_num++; + + return state; +} + + +// Release all resources on a state structure. +// Doesn't release the states, because it's part of a state block. +static void +kiss_thin_nfa_state_free(kiss_thin_nfa_state_t *state) +{ + // Clean up the pattern list + if (state->ids) { + kiss_thin_nfa_free_pattern_ids(state->ids); + state->ids = NULL; + } + + return; +} + + +// Returns the following state, by ID order. +// With prev==NULL, returns the first state. +// With prev!=NULL, returns the next. +// If prev is the last state, returns NULL. +static kiss_thin_nfa_state_t * +kiss_thin_nfa_get_subsequent_state(struct thin_nfa_comp_s *nfa_comp, kiss_thin_nfa_state_t *prev) +{ + static const char rname[] = "kiss_thin_nfa_get_subsequent_state"; + u_int state_id; + + // Find the next state's ID + state_id = prev ? prev->state_id + 1 : 0; + if (state_id >= nfa_comp->state_num) { + // prev was the last state. + return NULL; + } + + // Get the state pointer + return kiss_thin_nfa_get_state_by_id(nfa_comp, state_id, rname); +} + + +// Find the transition for a given character from a given state. +// If no transition found, returns NULL and does not check the fail state. +static kiss_thin_nfa_state_t * +kiss_thin_nfa_comp_get_next_state(kiss_thin_nfa_state_t *state, u_char ch) +{ + static const char rname[] = "kiss_thin_nfa_comp_get_next_state"; + kiss_thin_nfa_state_t *child; + + verify_state(state->comp, state); + + // Find the child in the list + for (child = state->child; child != NULL; child = child->sibling) { + u_char tran_ch = child->tran_char; + + if (tran_ch == ch) { + thinnfa_debug_extended(( + "%s: Found transition from the state %s by 0x%02x to %s\n", + rname, + state_name(state), + ch, + state_name(child) + )); + return child; + } + + // The list is sorted, so we don't need to look beyond the character. + if (tran_ch > ch) break; + } + + thinnfa_debug_extended(("%s: No transition from the state %s by 0x%02x\n", rname, state_name(state), ch)); + + return NULL; +} + + +// Mark a state as finite and accepting a given kiss_thin_nfa_pattern_t pattern +static kiss_ret_val +kiss_thin_nfa_state_set_match(kiss_thin_nfa_state_t *state, const kiss_thin_nfa_pattern_t *pat_info) +{ + static const char rname[] = "kiss_thin_nfa_state_set_match"; + + verify_state(state->comp, state); + + // Add the pattern to this state's pattern list + if (kiss_thin_nfa_add_pattern_id(&(state->ids), pat_info) != KISS_OK) { + thinnfa_debug_err(( + "%s: Could not add the 'pattern_id' %d to the final state %s\n", + rname, + pat_info->id, + state_name(state) + )); + return KISS_ERROR; + } + + thinnfa_debug(( + "Setting state %s as the matching state for the 'pattern_id' %d\n", + state_name(state), + pat_info->id + )); + make_state_matching(state); + + return KISS_OK; +} + + +// Mark a state as finite, and accepting a given kiss_pmglob_string pattern +static kiss_ret_val +kiss_thin_nfa_state_set_match_pattern(kiss_thin_nfa_state_t *state, const kiss_pmglob_string_s *pattern) +{ + kiss_thin_nfa_pattern_t pat_info; + + pat_info.id = kiss_pmglob_string_get_id(pattern); + pat_info.pattern_id_flags = kiss_pmglob_string_get_flags(pattern); + pat_info.len = kiss_pmglob_string_get_size(pattern); + + return kiss_thin_nfa_state_set_match(state, &pat_info); +} + + +// Copy the list of accepted patterns from one state to another. +// The destination state can already have patterns, and the lists would be concatenated. +static kiss_ret_val +kiss_thin_nfa_state_copy_match_ids(kiss_thin_nfa_state_t *dst, kiss_thin_nfa_state_t *src) +{ + static const char rname[] = "kiss_thin_nfa_state_copy_match_ids"; + kiss_thin_nfa_pattern_list_t *curr_id; + + verify_state(src->comp, src); + verify_state(dst->comp, dst); + + thinnfa_debug(("%s: Copying the match IDs from %s to %s\n", rname, state_name(src), state_name(dst))); + + // traversing on the state_src 'ids' adding each one to 'state_dst' list + for(curr_id = src->ids; curr_id; curr_id = curr_id->next) { + if (kiss_thin_nfa_state_set_match(dst, &curr_id->pattern) != KISS_OK) { + thinnfa_debug_err(( + "%s: Failed to set the ID %d on the state %s\n", + rname, + curr_id->pattern.id, + state_name(dst) + )); + + // NOTE: We don't release the IDs we have added. Compilation will fail and clean up anyway. + return KISS_ERROR; + } + } + + return KISS_OK; +} + + +// Destroy the NFA we're compiling +static void +kiss_thin_nfa_comp_destroy(struct thin_nfa_comp_s *nfa_comp) +{ + static const char rname[] = "kiss_thin_nfa_comp_destroy"; + u_int i; + kiss_thin_nfa_state_t *state; + + thinnfa_debug_major(("%s: Destroying the compilation information structure\n", rname)); + + // Cleanup whatever data we have on the states. + for (state = nfa_comp->root_state; state != NULL; state = kiss_thin_nfa_get_subsequent_state(nfa_comp, state)) { + kiss_thin_nfa_state_free(state); + } + + // Free the state blocks and transition blocks + for (i = 0; i < MAX_THIN_NFA_STATES_BLOCKS; i++) { + if (nfa_comp->state_blocks[i] != NULL) { + fw_kfree(nfa_comp->state_blocks[i], KISS_NFA_MAX_STATES_BLOCK_SIZE, rname); + nfa_comp->state_blocks[i] = NULL; + } + } + + if (nfa_comp->xlation_tab!= NULL) { + fw_kfree(nfa_comp->xlation_tab, sizeof(*(nfa_comp->xlation_tab)), rname); + nfa_comp->xlation_tab= NULL; + } + + nfa_comp->runtime_nfa.reset(nullptr); + + if (nfa_comp->patterns_hash) { + kiss_hash_destroy(nfa_comp->patterns_hash); + nfa_comp->patterns_hash = NULL; + } + + fw_kfree(nfa_comp, sizeof(*nfa_comp), rname); +} + + +// Allocate an empty thin NFA compilation data structure. +static struct thin_nfa_comp_s * +kiss_thin_nfa_comp_create(KissPMError *error) +{ + static const char rname[] = "kiss_thin_nfa_comp_create"; + struct thin_nfa_comp_s *nfa_comp = NULL; + + thinnfa_debug_major(("%s: Allocating the compilation information structure\n", rname)); + + // Allocate and initialize the compilation temporary structure + nfa_comp = (struct thin_nfa_comp_s *)fw_kmalloc(sizeof(*nfa_comp), rname); + if (!nfa_comp) { + thinnfa_debug_err(("%s: Failed to allocate 'nfa_comp'\n", rname)); + goto failure; + } + bzero((void *)nfa_comp, sizeof(*nfa_comp)); + + nfa_comp->error = error; + + // Build the root state + nfa_comp->root_state = kiss_thin_nfa_state_create(nfa_comp, 0, NULL, THIN_NFA_STATE_ROOT); + if (nfa_comp->root_state == NULL) { + thinnfa_debug_err(("%s: Failed to create the root state\n", rname)); + goto failure; + } + + return nfa_comp; + +failure: + + if (nfa_comp != NULL) { + kiss_thin_nfa_comp_destroy(nfa_comp); + } + return NULL; + +} + + +// Specify the error for failed Thin NFA compilation +static void +kiss_thin_nfa_set_comp_error(struct thin_nfa_comp_s *nfa_comp, const char *err_text) +{ + // We always use "internal", which is appropriate for both logical errors and resource shortage. + // We don't specify a pattern, because nothing is really pattern specific. + kiss_pm_error_set_details(nfa_comp->error, KISS_PM_ERROR_INTERNAL, err_text); +} + + +// Initialize a translation table for caseless/digitless comparison. +// According to compilation flags, builds a table to translate each character. +static kiss_ret_val +kiss_thin_nfa_create_xlation_tab(struct thin_nfa_comp_s *nfa_comp, int pm_comp_flags) +{ + static const char rname[] = "kiss_thin_nfa_create_xlation_tab"; + enum kiss_pmglob_char_xlation_flags_e xlation_flags; + + // Figure out which translations we need + xlation_flags = KISS_PMGLOB_CHAR_XLATION_NONE; + if (pm_comp_flags & KISS_PM_COMP_CASELESS) { + ENUM_SET_FLAG(xlation_flags, KISS_PMGLOB_CHAR_XLATION_CASE); + } + if (pm_comp_flags & KISS_PM_COMP_DIGITLESS) { + ENUM_SET_FLAG(xlation_flags, KISS_PMGLOB_CHAR_XLATION_DIGITS); + } + if (xlation_flags == KISS_PMGLOB_CHAR_XLATION_NONE) { + // No translation needed + nfa_comp->xlation_tab = NULL; + return KISS_OK; + } + + thinnfa_debug_major(("%s: Using%s%s translation table\n", rname, + (xlation_flags&KISS_PMGLOB_CHAR_XLATION_CASE) ? " caseless" : "", + (xlation_flags&KISS_PMGLOB_CHAR_XLATION_DIGITS) ? " digitless" : "")); + + // Allocate a translation table + nfa_comp->xlation_tab = (struct thin_nfa_char_trans_tab_s *)fw_kmalloc(sizeof(*(nfa_comp->xlation_tab)), rname); + if (!nfa_comp->xlation_tab) { + thinnfa_debug_err(("%s: Failed to allocate the translation table\n", rname)); + return KISS_ERROR; + } + + // Build the mapping - normal and reverse + kiss_pmglob_char_xlation_build(xlation_flags, nfa_comp->xlation_tab->tab); + kiss_pmglob_char_xlation_build_reverse(nfa_comp->xlation_tab->tab, nfa_comp->xlation_tab->rev); + + return KISS_OK; +} + + +// Translate a character to canonic form, if a translation table is defined. +static CP_INLINE u_char +kiss_thin_nfa_xlate_char(struct thin_nfa_comp_s *nfa_comp, u_char ch) +{ + if (!nfa_comp->xlation_tab) return ch; + return nfa_comp->xlation_tab->tab[ch]; +} + + +#if defined(KISS_THIN_NFA_DO_VERIFICATIONS) && !defined(KERNEL) + +// DEBUG FUNCTION - uses a simple&slow algorithm to verify the result of kiss_thin_nfa_are_trans_contained. +// Can't run in the kernel because of the large stack consumption. +static void +verify_trans_contains_( + kiss_thin_nfa_state_t *state_contains, + kiss_thin_nfa_state_t *state_included, + BOOL should_contain +) +{ + kiss_thin_nfa_state_t *trans_contains[KISS_PM_ALPHABET_SIZE]; + kiss_thin_nfa_state_t *trans_included[KISS_PM_ALPHABET_SIZE]; + kiss_thin_nfa_state_t *child; + u_int i; + int mismatch_pos; + + // Fill in both transition tables + bzero(trans_contains, sizeof(trans_contains)); + for (child = state_contains->child; child != NULL; child = child->sibling) { + trans_contains[child->tran_char] = child; + } + bzero(trans_included, sizeof(trans_included)); + for (child = state_included->child; child != NULL; child = child->sibling) { + trans_included[child->tran_char] = child; + } + + // Go over the table, looking for a character that's in "included" but not in "contains". + mismatch_pos = -1; + for (i=0; i %s), but the kiss_thin_nfa_are_trans_contained says it does", + state_name(state_contains), + state_name(state_included), + (u_char)mismatch_pos, + state_name(trans_included[i])) + ); + } +} + + +#define verify_trans_contains(state_contains, state_included, expected) \ + verify_trans_contains_(state_contains, state_included, expected) + +#else // KISS_THIN_NFA_DO_VERIFICATIONS + +#define verify_trans_contains(state_contains, state_included, expected) + +#endif // KISS_THIN_NFA_DO_VERIFICATIONS + + +// Do all transactions of "included" also exist in "contains"? +static BOOL +kiss_thin_nfa_are_trans_contained(kiss_thin_nfa_state_t *state_contains, kiss_thin_nfa_state_t *state_included) +{ + kiss_thin_nfa_state_t *included_child, *contains_child; + + verify_state(state_contains->comp, state_contains); + verify_state(state_included->comp, state_included); + + if (state_contains->num_trans < state_included->num_trans) { + // "contains" has fewer states - it can't include all "included" + verify_trans_contains(state_contains, state_included, FALSE); + return FALSE; + } + + // Advance both included_child and contains_child, to iterate both transition tables. + // Keep them in sync - included_child passes children one by one, and contains_child is advanced + // to the same transition character at each step. + contains_child = state_contains->child; + + // Go over the transitions in "included", see if they're in "contained" + for (included_child = state_included->child; included_child != NULL; included_child = included_child->sibling) { + // Advance "tran_contains" until we reach the character we want + for (; contains_child != NULL; contains_child = contains_child->sibling) { + if (contains_child->tran_char >= included_child->tran_char) break; + } + + // Do we have this character in "contains"? + if (contains_child == NULL || contains_child->tran_char != included_child->tran_char) { + // This character doesn't exist in state_contains + verify_trans_contains(state_contains, state_included, FALSE); + return FALSE; + } + } + + verify_trans_contains(state_contains, state_included, TRUE); + return TRUE; +} + + +// Get the root state, or the anchored root state, as appropriate for the pattern. +static kiss_thin_nfa_state_t * +kiss_thin_nfa_get_root_state(struct thin_nfa_comp_s *nfa_comp, int anchored) +{ + static const char rname[] = "kiss_thin_nfa_get_root_state"; + + if (!anchored || !(nfa_comp->flags & THIN_NFA_ENABLE_ANCHOR_OPT)) { + thinnfa_debug(("%s: Using normal root: %s, feature %s\n", rname, + anchored?"anchored":"not anchored", (nfa_comp->flags & THIN_NFA_ENABLE_ANCHOR_OPT)?"enabled":"disabled")); + return nfa_comp->root_state; + } + + if (!nfa_comp->anchored_root_state) { + // Lazy creation of the anchored root state + nfa_thin_state_flags_t flags = THIN_NFA_STATE_ROOT; + ENUM_SET_FLAG(flags, THIN_NFA_STATE_ANCHORED); + thinnfa_debug(("%s: Creating a new anchored root\n", rname)); + nfa_comp->anchored_root_state = kiss_thin_nfa_state_create(nfa_comp, 0, NULL, flags); + if (nfa_comp->anchored_root_state == NULL) { + thinnfa_debug_err(("%s: Failed to create the anchored root state\n", rname)); + return NULL; + } + } + + thinnfa_debug(("%s: Returning the anchored root (%d)\n", rname, nfa_comp->anchored_root_state->state_id)); + return nfa_comp->anchored_root_state; +} + + +// Find the state in the trie, which represents the longest prefix of a given string. +static kiss_thin_nfa_state_t * +kiss_thin_nfa_find_longest_prefix(struct thin_nfa_comp_s *nfa_comp, const u_char *text, u_int len, int anchored) +{ + u_int offset; + kiss_thin_nfa_state_t *state; + + // Following the path labeled by chars in 'pattern' (skip the states which already exist) + state = kiss_thin_nfa_get_root_state(nfa_comp, anchored); + if (!state) return NULL; + for (offset = 0; offset < len; offset++) { + kiss_thin_nfa_state_t *next_state; + u_char ch = kiss_thin_nfa_xlate_char(nfa_comp, text[offset]); + + verify_state(nfa_comp, state); + + // Do we have a node for the next character? + next_state = kiss_thin_nfa_comp_get_next_state(state, ch); + + if (next_state == NULL) { + // No next state - this is as far as we go + break; + } else { + state = next_state; + } + } + + return state; +} + + +// Add a newly allocated state to the trie. Keep the transition list sorted. +static void +kiss_thin_nfa_add_transition(kiss_thin_nfa_state_t *parent, u_char tran_char, kiss_thin_nfa_state_t *new_child) +{ + static const char rname[] = "kiss_thin_nfa_add_transition"; + kiss_thin_nfa_state_t **child_p; + + // Go over existing children and find the place to add the transition + for (child_p = &parent->child; *child_p != NULL; child_p = &(*child_p)->sibling) { + kiss_thin_nfa_state_t *child = *child_p; + if (child->tran_char > tran_char) { + // Add before this one + break; + } + } + + // Add the transition + new_child->sibling = *child_p; + *child_p = new_child; + new_child->tran_char = tran_char; + parent->num_trans++; + + thinnfa_debug_extended(("%s: Added transition from %s by 0x%2x to %s\n", rname, + state_name(parent), tran_char, state_name(new_child))); + + if (parent->num_trans > MIN(kiss_thin_nfa_max_partial_trans, KISS_BNFA_MAX_TRANS_NUM)) { + thinnfa_debug(( + "%s: State %s has %d transitions - making it full\n", + rname, + state_name(parent), + parent->num_trans + )); + make_state_full(parent); + } + + // Track states which represent a maximal sequence of identical characters + if ((parent->flags & THIN_NFA_STATE_ROOT) && !(parent->flags & THIN_NFA_STATE_ANCHORED)) { + // Single character - all characters are identical + ENUM_SET_FLAG(new_child->flags, THIN_NFA_STATE_MAX_IDENTICAL_CHAR); + } else if ((parent->flags & THIN_NFA_STATE_MAX_IDENTICAL_CHAR) && (parent->tran_char == tran_char)) { + // The child, not the parent, is now the longest + ENUM_UNSET_FLAG(parent->flags, THIN_NFA_STATE_MAX_IDENTICAL_CHAR); + ENUM_SET_FLAG(new_child->flags, THIN_NFA_STATE_MAX_IDENTICAL_CHAR); + } +} + + +// Add a pattern to the trie, which would generate the Thin NFA. +// Upon failure, doesn't clean up states it may have created. Will be cleaned up when destroying nfa_comp. +static kiss_ret_val +kiss_thin_nfa_add_pattern_to_trie(struct thin_nfa_comp_s *nfa_comp, const kiss_pmglob_string_s *sm_cur_pattern) +{ + static const char rname[] = "kiss_thin_nfa_add_pattern_to_trie"; + const u_char *pattern_text; + u_int pattern_len; + u_int i; + kiss_thin_nfa_state_t *current_state; + int anchored_pattern; + + pattern_text = kiss_pmglob_string_get_pattern(sm_cur_pattern); + pattern_len = kiss_pmglob_string_get_size(sm_cur_pattern); + anchored_pattern = kiss_pmglob_string_get_flags(sm_cur_pattern) & KISS_PM_LSS_AT_BUF_START; + + thinnfa_debug(("%s: Adding the pattern: %s flags=%x\n", rname, kiss_pmglob_string_to_debug_charp(sm_cur_pattern), + kiss_pmglob_string_get_flags(sm_cur_pattern))); + + // How much of this pattern do we already have in the tree? + current_state = kiss_thin_nfa_find_longest_prefix(nfa_comp, pattern_text, pattern_len, anchored_pattern); + if (!current_state) return KISS_ERROR; // Messages printed inside + + thinnfa_debug(("%s: State %s (flags %x) represents the longest prefix at the offset %d/%d\n", rname, + state_name(current_state), current_state->flags, current_state->depth, pattern_len)); + + // Go over the remaining bytes (if any) and add more states + for (i = current_state->depth; i < pattern_len; i++) { + kiss_thin_nfa_state_t *new_state; + u_char ch; + + // Create a new state. Depth i+1, because the first character (i=0) is at depth 1. + new_state = kiss_thin_nfa_state_create(nfa_comp, i+1, pattern_text, + (nfa_thin_state_flags_t)(current_state->flags & THIN_NFA_STATE_ANCHORED)); + if (!new_state) { + thinnfa_debug_err(("%s: Failed to allocate a new state\n", rname)); + kiss_thin_nfa_set_comp_error(nfa_comp, "Failed to allocate a new state"); + return KISS_ERROR; + } + + // Add a transition into the new state + ch = kiss_thin_nfa_xlate_char(nfa_comp, pattern_text[i]); + kiss_thin_nfa_add_transition(current_state, ch, new_state); + + thinnfa_debug(("%s: Added new state+transition %s -> %s by 0x%02x offset %d\n", rname, + state_name(current_state), state_name(new_state), ch, i)); + + verify_state(nfa_comp, current_state); + + // Add the following states after this one + current_state = new_state; + } + + // Set state as finite and add the pattern ID to the list of patterns which this state accepts. + // Note: It's OK if the state isn't one we just added. E.g. the new pattern is a prefix of an existing one. + if (kiss_thin_nfa_state_set_match_pattern(current_state, sm_cur_pattern) != KISS_OK) { + thinnfa_debug_err(( + "%s: Failed to save the pattern information for the state %s\n", + rname, + state_name(current_state) + )); + kiss_thin_nfa_set_comp_error(nfa_comp, "Failed to save the pattern information for the state"); + return KISS_ERROR; + } + + return KISS_OK; +} + + +// Find the transition from a state by a character, considering fail states. +// The state should alrady have its fail state calculated. +// +// Note: kiss_bnfa_build_full_trans_table may pass from_state=NULL. The result is returning the root, which is OK. +static kiss_thin_nfa_state_t * +kiss_thin_nfa_calc_transition(struct thin_nfa_comp_s *nfa_comp, kiss_thin_nfa_state_t *from_state, u_char tran_char) +{ + static const char rname[] = "kiss_thin_nfa_calc_transition"; + kiss_thin_nfa_state_t *state; + + // Go down the fail state chain, until we find a transition. + for (state = from_state; state != NULL; state = state->fail_state) { + kiss_thin_nfa_state_t *next_state; + + // Look up in this state's transition table + next_state = kiss_thin_nfa_comp_get_next_state(state, tran_char); + if (next_state != NULL) { + if (state == from_state) { + thinnfa_debug_extended(("%s: Found transition from %s by 0x%02x to %s\n", rname, + state_name(from_state), tran_char, state_name(next_state))); + } else { + thinnfa_debug_extended(( + "%s: Found transition from %s by 0x%02x to %s using the fail state %s\n", + rname, + state_name(from_state), + tran_char, + state_name(next_state), + state_name(state) + )); + } + return next_state; + } + } + + // We've gone down to the root, and found nothing - so the next state is the root. + thinnfa_debug_extended(("%s: No transition from %s by 0x%02x - going to root\n", rname, + state_name(from_state), tran_char)); + return nfa_comp->root_state; +} + + +// A callback function prototype for kiss_thin_nfa_iterate_trans +typedef kiss_ret_val (*kiss_thin_nfa_iterate_trans_cb)(kiss_thin_nfa_state_t *from_state, + u_char tran_char, kiss_thin_nfa_state_t *to_state); + + +// Iterate all the transitions in the trie, in BFS order. +// Note: The callback will be called once per transition, i.e. once per state, except for the initial state. +static kiss_ret_val +kiss_thin_nfa_iterate_trans_bfs(struct thin_nfa_comp_s *nfa_comp, kiss_thin_nfa_iterate_trans_cb iter_cb) +{ + static const char rname[] = "kiss_thin_nfa_iterate_trans_bfs"; + kiss_thin_nfa_state_t *bfs_q_head, *bfs_q_tail; + + thinnfa_debug(("%s: Starting BFS iteration, %d states\n", rname, nfa_comp->state_num)); + + // This queue contains states, whose children we want to iterate. + // We start with the root state followed by the anchored root state. + bfs_q_head = nfa_comp->root_state; + bfs_q_head->bfs_q = NULL; + bfs_q_tail = bfs_q_head; + if (nfa_comp->anchored_root_state) { + bfs_q_tail->bfs_q = nfa_comp->anchored_root_state; + nfa_comp->anchored_root_state->bfs_q = NULL; + bfs_q_tail = nfa_comp->anchored_root_state; + } + + // Dequeue each of the states, call the iterator for each transition and enqueue the children + while (bfs_q_head != NULL) { + kiss_thin_nfa_state_t *from_state; + kiss_thin_nfa_state_t *to_state; + + // Dequeue a state from the head + from_state = bfs_q_head; + bfs_q_head = from_state->bfs_q; + if (bfs_q_head == NULL) bfs_q_tail = NULL; + + thinnfa_debug_extended(( + "%s: Got the state %s with %d children\n", + rname, + state_name(from_state), + from_state->num_trans + )); + + // Go over the state's transitions + for (to_state = from_state->child; to_state != NULL; to_state = to_state->sibling) { + thinnfa_debug_extended(( + "%s: Got the child state %s at the depth %d\n", + rname, + state_name(to_state), + to_state->depth + )); + + // Call the iterator function + if (iter_cb(from_state, to_state->tran_char, to_state) != KISS_OK) { + return KISS_ERROR; + } + + // No need to enqueue states with no children + if (to_state->num_trans == 0) continue; + + // Enqueue the next state, so we'd iterate its transitions too + to_state->bfs_q = NULL; + if (bfs_q_tail != NULL) { + bfs_q_tail->bfs_q = to_state; + } else { + bfs_q_head = to_state; + } + bfs_q_tail = to_state; + } + } + + return KISS_OK; +} + + +// Set a state's fail state. +// To calculate this, we need the state's parent, and the character that takes us from the parent to the current. +// The parent's fail state must be calculated already. +static kiss_ret_val +kiss_thin_nfa_set_fail_state(kiss_thin_nfa_state_t *parent, u_char tran_char, kiss_thin_nfa_state_t *state) +{ + static const char rname[] = "kiss_thin_nfa_set_fail_state"; + kiss_thin_nfa_state_t *fail_state; + + // Calculate the fail state. + // The same character that takes us from parent to state would take us from parent->fail_state to state->fail_state + fail_state = kiss_thin_nfa_calc_transition(state->comp, parent->fail_state, tran_char); + state->fail_state = fail_state; + + thinnfa_debug(("%s: The fail state of %s is %s (parent %s, parent->fail_state %s, char %02x)\n", rname, + state_name(state), state_name(fail_state), state_name(parent), + state_name(parent->fail_state), tran_char)); + + + // If a state's fail state is finite, so is the state itself. + // This is because the fail state represents a suffix of the state, which is included in + // the suffix the state represents. If the shorter suffix is a match, so is the longer one. + // Example - The fail state of "abc" is "bc" (if it exists). If "bc" is a match, then so is "abc". + if (fail_state->flags & THIN_NFA_STATE_MATCH) { + thinnfa_debug(("%s: Fail state %s is finite - so is %s\n", rname, + state_name(fail_state), state_name(state))); + if (kiss_thin_nfa_state_copy_match_ids(state, fail_state)) { + thinnfa_debug_err(( + "%s: Failed to copy the pattern IDs from %s to %s\n", + rname, + state_name(fail_state), + state_name(state) + )); + kiss_thin_nfa_set_comp_error(state->comp, "Failed to copy the pattern IDs"); + return KISS_ERROR; + } + } + + // This isn't related to calculating fail states. It should be done after the trie was built, but before + // starting BNFA construction. + if (kiss_thin_nfa_optimize_contig_chars && (state->flags & THIN_NFA_STATE_MAX_IDENTICAL_CHAR)) { + // Optimization for identical character sequences. States which represent a maximal sequence of the same + // characters will be full. So for a long sequence of a single character, we'll always be in a full state. + // Great for the performance lab. + thinnfa_debug(( + "%s: State %s is a maximal identical character sequence - making it full\n", + rname, + state_name(state) + )); + make_state_full(state); + } + + return KISS_OK; +} + + +// See if we can find a better fail state for a state. +// If the fail state contains only transitions the original state has anyway, we can use its fail state instead. +static kiss_thin_nfa_state_t * +kiss_thin_nfa_find_better_fail_state(kiss_thin_nfa_state_t *state) +{ + kiss_thin_nfa_state_t *fail_state; + + if (!state->fail_state) return NULL; + + // Go down the fail state chain. + // Keep going as long as the states contain only transitions the current state has anyway. + for (fail_state = state->fail_state; fail_state->fail_state != NULL; fail_state = fail_state->fail_state) { + + verify_state(state->comp, fail_state); + + if (fail_state->flags & THIN_NFA_STATE_FULL) { + // Full state - failing to it will always give us the answer. + break; + } + + if (!kiss_thin_nfa_are_trans_contained(state, fail_state)) { + // This state has transitions that the current state doesn't - we must fail to it, + // not lower. + break; + } + } + + return fail_state; +} + + +// Change fail states to go faster up the tree, if possible. +// Normally, a fail state points one level upward. But sometimes it can be more upward. +// +// Note: This must be done after kiss_thin_nfa_set_fail_state was called for all states. This is because +// kiss_thin_nfa_set_fail_state uses the parent's fail state to calculate the child's. If the parent's fail stae +// was "reduced", we'll get the wrong fail state for the child. +static void +kiss_thin_nfa_reduce_fail_states(struct thin_nfa_comp_s *nfa_comp) +{ + static const char rname[] = "kiss_thin_nfa_reduce_fail_states"; + kiss_thin_nfa_state_t *state; + + for (state = nfa_comp->root_state; state != NULL; state = kiss_thin_nfa_get_subsequent_state(nfa_comp, state)) { + kiss_thin_nfa_state_t *fail_state; + + if (state->flags & THIN_NFA_STATE_FULL) { + // A full state's fail state isn't interesting + continue; + } + + fail_state = kiss_thin_nfa_find_better_fail_state(state); + if (fail_state != state->fail_state) { + // We have a better fail state + thinnfa_debug(("%s: Changing the fail state of %s from %s to %s\n", rname, + state_name(state), state_name(state->fail_state), state_name(fail_state))); + state->fail_state = fail_state; + } + } +} + + +// Calculate fail states for all states. +static kiss_ret_val +kiss_thin_nfa_calc_fail_states(struct thin_nfa_comp_s *nfa_comp) +{ + static const char rname[] = "kiss_thin_nfa_calc_fail_states"; + + // The root state has no fail state + nfa_comp->root_state->fail_state = NULL; + if (nfa_comp->anchored_root_state) { + // The anchored root fails to the root + nfa_comp->anchored_root_state->fail_state = nfa_comp->root_state; + } + + thinnfa_debug(("%s: Calculating the fail states for all states\n", rname)); + + // Iterate all transitions, and calculate fail states for the target states. + // This would cover all states, except the initial (whose fail state was already set). + // BFS order assures that a parent's fail state is already calculated when we reach the child. + if (kiss_thin_nfa_iterate_trans_bfs(nfa_comp, kiss_thin_nfa_set_fail_state) != KISS_OK) { + thinnfa_debug_err(("%s: Failed to calculate the fail states\n", rname)); + return KISS_ERROR; + } + + // All states now have their fail states calculated + ENUM_SET_FLAG(nfa_comp->flags, THIN_NFA_FAIL_STATES_CALCULATED); + + // Optimization - reduce fail states + kiss_thin_nfa_reduce_fail_states(nfa_comp); + + return KISS_OK; +} + + +// Set a state's BNFA offset to the size so far, and increment by the state size. +static void +set_state_offset(kiss_thin_nfa_state_t *state, kiss_bnfa_offset_t *cur_offset) +{ + static const char rname[] = "set_state_offset"; + u_int state_size=0, match_size=0; + + verify_state(state->comp, state); + + if (state->bnfa_offset == KISS_BNFA_OFFSET_INVALID) { + // Room for the actual state - negative offset for full states, positive for partial. + if ((state->flags & THIN_NFA_STATE_FULL) && (*cur_offset<0)) { + state_size = sizeof(kiss_bnfa_full_state_t); + } else if (!(state->flags & THIN_NFA_STATE_FULL) && (*cur_offset>=0)) { + state_size = kiss_bnfa_partial_state_size(state->num_trans); + } + } + + if (state->bnfa_incoming_off == KISS_BNFA_OFFSET_INVALID) { + // Room for a match state - if needed, must be a positive offset. + if ((state->flags & THIN_NFA_STATE_MATCH) && (*cur_offset >= 0)) { + match_size = sizeof(kiss_bnfa_match_state_t); + if (state->flags & THIN_NFA_STATE_FULL) { + // Need a jump state too + match_size += kiss_bnfa_partial_state_size(0); + } + } + } + + // Update the state offsets + if (match_size > 0) { + thinnfa_debug_extended(("%s: State %s was given a match offset %d size %d", rname, state_name(state), + *cur_offset, match_size)); + state->bnfa_incoming_off = *cur_offset; + *cur_offset += match_size; + } + if (state_size > 0) { + thinnfa_debug_extended(("%s: State %s was given a real offset %d size %d", rname, state_name(state), + *cur_offset, state_size)); + state->bnfa_offset = *cur_offset; + *cur_offset += state_size; + if (!(state->flags & THIN_NFA_STATE_MATCH)) { + // Incoming transitions go directly to the state + state->bnfa_incoming_off = state->bnfa_offset; + } + } +} + +// Check if compressed offset fits full state offset size +static BOOL +comp_offset_fits_short(kiss_bnfa_comp_offset_t comp_offset) +{ + if ((comp_offset) != (kiss_bnfa_short_offset_t)(comp_offset)) { + return FALSE; + } + return TRUE; +} + +// Mark all child of a given state as reacheable as reachable from full state +static void +kiss_bnfa_mark_childs_reach_from_full(kiss_thin_nfa_state_t *state) +{ + kiss_thin_nfa_state_t *child; + + for (child = state->child; child != NULL; child = child->sibling) { + ENUM_SET_FLAG(child->flags, THIN_NFA_STATE_REACH_FROM_FULL); + } +} + +// Mark all states that are reachable from a given full state, +// in order to place them at lower offsets to avoid possible overflow due to offset compression. +// If a state`s fail state is of partial type, mark it`s children too +static void +kiss_bnfa_mark_reachable_from_full(kiss_thin_nfa_state_t *state) { + + kiss_bnfa_mark_childs_reach_from_full(state); + for (state = state->fail_state; state && !(state->flags & THIN_NFA_STATE_FULL); state = state->fail_state) { + kiss_bnfa_mark_childs_reach_from_full(state); + } +} + +// Calcultate the offset of each BNFA state, and the entire BNFA size. +// Sets nfa_comp->offset_list to an array, holding the BNFA offset for each state at [state_id]. +// Sets *bnfa_size_p to the total BNFA size. +static kiss_ret_val +kiss_bnfa_calc_offsets(struct thin_nfa_comp_s *nfa_comp) +{ + static const char rname[] = "kiss_bnfa_calc_offsets"; + kiss_thin_nfa_state_t *state; + kiss_bnfa_offset_t cur_offset; + + // Full states have negative offsets. So the first state's offset depends on the number of full states. + cur_offset = -(kiss_bnfa_offset_t)(nfa_comp->full_state_num * sizeof(kiss_bnfa_full_state_t)); + nfa_comp->min_bnfa_off = cur_offset; + + // Put the anchored root state first, because it's the initial state + if (nfa_comp->anchored_root_state) { + KISS_ASSERT(nfa_comp->anchored_root_state->flags & THIN_NFA_STATE_FULL, + "%s: The anchored root %s must be a full state\n", rname, state_name(nfa_comp->anchored_root_state)); + set_state_offset(nfa_comp->anchored_root_state, &cur_offset); + } + + // If there's no anchored root, then root must be initial. If there is, validation expects it second. + set_state_offset(nfa_comp->root_state, &cur_offset); + + // in this loop we add only the full states, which have negative offsets + for (state = nfa_comp->root_state; state != NULL; state = kiss_thin_nfa_get_subsequent_state(nfa_comp, state)) { + if (state->flags & THIN_NFA_STATE_FULL) { + kiss_bnfa_mark_reachable_from_full(state); // Mark child states so they'll get low offsets + set_state_offset(state, &cur_offset); + } + } + // We added all full states and moving to partials - we must be at offset 0. + KISS_ASSERT(cur_offset==0, + "%s: Offset %d != 0 after adding %d full states\n", rname, cur_offset, nfa_comp->full_state_num); + + // in this loop we add states that are reachable from full states. We want them at low offsets to avoid + // possible overflow due to offset compression + for (state = nfa_comp->root_state; state != NULL; state = kiss_thin_nfa_get_subsequent_state(nfa_comp, state)) { + if (state->flags & THIN_NFA_STATE_REACH_FROM_FULL){ + set_state_offset(state, &cur_offset); + } + } + + // Make sure we have not exceede the limit of offsets that can be compressed to 16bit + // Note: the test is a little too strict - we check the first state that is not reachable from a full state + // instead of the last state that is reachable + if (!comp_offset_fits_short(kiss_bnfa_offset_compress(cur_offset))) { + thinnfa_debug_err(("%s: Current offset is %d, not reachable from the full state\n", rname, cur_offset)); + kiss_thin_nfa_set_comp_error(nfa_comp, "Exceeded the limit of reachable states"); + return KISS_ERROR; + } + + // in this loop we add the partial and mathing states, which weren't handled in the loop above. + for (state = nfa_comp->root_state; state != NULL; state = kiss_thin_nfa_get_subsequent_state(nfa_comp, state)) { + set_state_offset(state, &cur_offset); + } + // The current offset is the size of partial states. Add the full state size to get the total size. + nfa_comp->max_bnfa_off = cur_offset; + + thinnfa_debug_major(("%s: BNFA size - %u full states, %u partial states, total %u bytes\n", rname, + nfa_comp->full_state_num, + nfa_comp->state_num-nfa_comp->full_state_num, + nfa_comp->max_bnfa_off - nfa_comp->min_bnfa_off)); + + return KISS_OK; +} + + +// Get a state's BNFA offset. +// skip_match makes a difference for matching states: +// TRUE - Get the actual state, where the transition table is. +// FALSE - Get the match state, where incoming transitions should go. +static kiss_bnfa_offset_t +state_bnfa_offset(kiss_thin_nfa_state_t *state, BOOL skip_match) +{ + return skip_match ? state->bnfa_offset : state->bnfa_incoming_off; +} + + +// Convert a BNFA offset to a BNFA state pointer +static kiss_bnfa_state_t * +comp_bnfa_offset_to_state(struct thin_nfa_comp_s *nfa_comp, kiss_bnfa_offset_t bnfa_offset) +{ + return kiss_bnfa_offset_to_state_write(nfa_comp->runtime_nfa->bnfa, bnfa_offset); +} + + +// Get a pointer to a state in the BNFA. +// skip_match makes a difference for matching states: +// TRUE - Get the actual state, where the transition table is. +// FALSE - Get the match state, where incoming transitions should go. +static kiss_bnfa_state_t * +comp_to_bnfa_state(kiss_thin_nfa_state_t *state, BOOL skip_match) +{ + return comp_bnfa_offset_to_state(state->comp, state_bnfa_offset(state, skip_match)); +} + +// Move next to state_bnfa_offset. assert inside. +static kiss_bnfa_short_offset_t +state_bnfa_short_offset(kiss_thin_nfa_state_t *state) +{ + static const char rname[] = "state_bnfa_short_offset"; + kiss_bnfa_comp_offset_t comp_offset = kiss_bnfa_offset_compress(state_bnfa_offset(state, FALSE)); + + KISS_ASSERT(comp_offset_fits_short(comp_offset), + "%s: Compressed offset %d exceeds the allowed size\n", rname, comp_offset); + + return (kiss_bnfa_short_offset_t) comp_offset; +} + + +// If character translation is enabled, duplicate ch's transition to all equivalents +static void +add_equivalent_transitions(struct thin_nfa_comp_s *nfa_comp, kiss_bnfa_full_state_t *bnfa_state, u_char ch) +{ + static const char rname[] = "add_equivalent_transitions"; + u_char other_ch; + u_int group_size; + + if (!nfa_comp->xlation_tab) return; + + // Go over all characters within the same group + group_size = 0; + for (other_ch = nfa_comp->xlation_tab->rev[ch]; other_ch != ch; other_ch = nfa_comp->xlation_tab->rev[other_ch]) { + thinnfa_debug_extended(("%s: Setting translated transition by %02x - same as %02x\n", rname, other_ch, ch)); + + bnfa_state->transitions[other_ch] = bnfa_state->transitions[ch]; + + // Prevent looping in case the table is corrupt + group_size++; + KISS_ASSERT_CRASH(group_size <= KISS_PM_ALPHABET_SIZE, + "%s: Too many characters to translate into %02x\n", rname, ch); + } +} + + +// Add a transition to a full transition table. +// If there's a translation table, add trnasitions for all equivalent characters. +static void +add_full_transition( + struct thin_nfa_comp_s *nfa_comp, + kiss_bnfa_full_state_t *bnfa_state, + kiss_thin_nfa_state_t *next_state +) +{ + static const char rname[] = "add_full_transition"; + u_char ch = next_state->tran_char; + + thinnfa_debug_extended(("%s: Setting the transition by %02x to %s\n", rname, + next_state->tran_char, state_name(next_state))); + + // Set the transition, for ch and equivalent characters + bnfa_state->transitions[ch] = state_bnfa_short_offset(next_state); + add_equivalent_transitions(nfa_comp, bnfa_state, ch); +} + +#if !defined(KERNEL) + +// A recursive algorithm to build full state tables. +// Much faster than the previous algorithm, but shouldn't be used in the kernel. +// Allow mutual recursion between these two functions: +static void build_full_trans_table(kiss_thin_nfa_state_t *comp_state); +static void get_full_trans_table(kiss_thin_nfa_state_t *target_state, kiss_thin_nfa_state_t *source_state); + + +// Get the transition table of source_state and write it in target_state's. +// source_state is somewhere in the fail state chain of target_state. +static void +get_full_trans_table(kiss_thin_nfa_state_t *target_state, kiss_thin_nfa_state_t *source_state) +{ + kiss_thin_nfa_state_t *child; + kiss_bnfa_state_t *target_bnfa = comp_to_bnfa_state(target_state, TRUE); + + if (source_state != target_state && (source_state->flags & THIN_NFA_STATE_FULL)) { + // We've reached a full state - just copy its transition table (build it first, if needed) + build_full_trans_table(source_state); + bcopy(comp_to_bnfa_state(source_state, TRUE)->full.transitions, + target_bnfa->full.transitions, + sizeof(target_bnfa->full.transitions)); + return; + } + + // Start with our fail state's state table + if (source_state->fail_state) { + get_full_trans_table(target_state, source_state->fail_state); + } else { + int i; + kiss_bnfa_short_offset_t root_bnfa_comp_offset = state_bnfa_short_offset(source_state); + + // Reached the root - fill with transitions to root + for (i=0; ifull.transitions[i] = root_bnfa_comp_offset; + } + } + + // Override transitions which exist in this state + for (child = source_state->child; child != NULL; child = child->sibling) { + add_full_transition(target_state->comp, &target_bnfa->full, child); + } +} + + +// Recursive function for building a full state's state table. +// target_bnfa_state is the state who's table we're building. +// source_state changes when recursing over the tail state chain +static void +build_full_trans_table(kiss_thin_nfa_state_t *comp_state) +{ + if (comp_state->flags & THIN_NFA_STATE_BUILT_TABLE) return; + + get_full_trans_table(comp_state, comp_state); + + ENUM_SET_FLAG(comp_state->flags, THIN_NFA_STATE_BUILT_TABLE); +} + +#endif // KERNEL + +static CP_INLINE kiss_ret_val +verify_add_state(kiss_thin_nfa_state_t *comp_state, kiss_bnfa_state_t *bnfa_state, u_int state_size, + const char *caller, const char *type) +{ + const KissThinNFA *nfa_h = comp_state->comp->runtime_nfa.get(); + kiss_bnfa_offset_t bnfa_offset = (char *)bnfa_state - (char *)(nfa_h->bnfa); + u_int state_alignment = (bnfa_offset < 0) ? sizeof(kiss_bnfa_full_state_t) : KISS_BNFA_STATE_ALIGNMENT; + + if ((bnfa_offset < nfa_h->min_bnfa_offset) || (bnfa_offset+(int)state_size > nfa_h->max_bnfa_offset)) { + thinnfa_debug_err(("%s: Cannot add the %s state %s at the offset %d:%d - out of range %d:%d\n", caller, type, + state_name(comp_state), + bnfa_offset, bnfa_offset+state_size, + nfa_h->min_bnfa_offset, nfa_h->max_bnfa_offset)); + return KISS_ERROR; + } + + if ((bnfa_offset % state_alignment) != 0) { + thinnfa_debug_err(( + "%s: Cannot add the %s state %s at the offset %d:%d - not aligned on %d bytes\n", + caller, + type, + state_name(comp_state), + bnfa_offset, + bnfa_offset + state_size, + state_alignment + )); + return KISS_ERROR; + } + + thinnfa_debug(("%s: Adding the %s state %s, offsets %d:%d\n", caller, type, + state_name(comp_state), + bnfa_offset, bnfa_offset+state_size)); + + return KISS_OK; +} + + +// Old, non-recursive and slow version on build_full_trans_table. +static void +build_full_trans_table_no_recursion(kiss_thin_nfa_state_t *comp_state) +{ + static const char rname[] = "build_full_trans_table_no_recursion"; + struct thin_nfa_comp_s *nfa_comp = comp_state->comp; + kiss_bnfa_state_t *bnfa_state = comp_to_bnfa_state(comp_state, TRUE); + kiss_thin_nfa_state_t *child; + u_int i; + + // Go over all characters. Maintain a pointer to the next transition in the list. + // We rely on the list being sorted. + // We could simply call kiss_thin_nfa_calc_transition for each character. But it would look up again and + // again in the current state. + child = comp_state->child; + for (i = 0; i < KISS_PM_ALPHABET_SIZE; i++) { + u_char ch = (u_char)i; + kiss_thin_nfa_state_t *next_state; + + // Check if it's a canonic character (e.g. lowercase when we're case insensitive) + if (kiss_thin_nfa_xlate_char(nfa_comp, ch) != ch) { + // We'll fill this in when we reach the canonic character. + continue; + } + + if (child != NULL && child->tran_char == ch) { + // Use the explicit transition + next_state = child; + + // Go forward in the transition table + child = child->sibling; + + thinnfa_debug_extended(("%s: Setting the explicit transition by %02x to %s\n", rname, + ch, state_name(next_state))); + } else { + // Note: if comp_state is the initial, we pass from_state=NULL. + // This works as desired (returning the initial state). + next_state = kiss_thin_nfa_calc_transition(nfa_comp, comp_state->fail_state, ch); + + thinnfa_debug_extended(("%s: Setting the fail-state transition by %02x to %s\n", rname, + ch, state_name(next_state))); + } + + // Set the transition for this character and equivalents + bnfa_state->full.transitions[ch] = state_bnfa_short_offset(next_state); + add_equivalent_transitions(nfa_comp, &bnfa_state->full, ch); + } + ENUM_SET_FLAG(comp_state->flags, THIN_NFA_STATE_BUILT_TABLE); +} + + +// Build a full state's transition table in the BNFA. +// Either uses the explicit transition, or calculates using fail states. +static kiss_ret_val +kiss_bnfa_build_full_state(kiss_thin_nfa_state_t *comp_state) +{ + static const char rname[] = "kiss_bnfa_build_full_state"; + kiss_bnfa_state_t *bnfa_state = comp_to_bnfa_state(comp_state, TRUE); + + if (verify_add_state(comp_state, bnfa_state, sizeof(kiss_bnfa_full_state_t), rname, "full") != KISS_OK) { + return KISS_ERROR; + } + +#if !defined(KERNEL) + if (comp_state->comp->flags & THIN_NFA_USE_RECURSIVE_COMPILE) { + build_full_trans_table(comp_state); + return KISS_OK; + } +#endif // KERNEL + + build_full_trans_table_no_recursion(comp_state); + + return KISS_OK; +} + + +static void +kiss_bnfa_build_partial_state_header( + kiss_bnfa_partial_state_t *bnfa_state, + u_int trans_num, + kiss_bnfa_offset_t fail_offset +) +{ + bnfa_state->type = KISS_BNFA_STATE_PARTIAL; + bnfa_state->trans_num = trans_num; + bnfa_state->fail_state_offset = kiss_bnfa_offset_compress(fail_offset); +} + + +// Build a partial state's transition table in the BNFA. +// Temporary encoding - sets the state ID instead of the BNFA offset (which is yet unknown). +static kiss_ret_val +kiss_bnfa_build_partial_state(kiss_thin_nfa_state_t *comp_state) +{ + static const char rname[] = "kiss_bnfa_build_partial_state"; + kiss_bnfa_state_t *bnfa_state = comp_to_bnfa_state(comp_state, TRUE); + kiss_thin_nfa_state_t *child; + u_int trans_num; + + if (verify_add_state( + comp_state, + bnfa_state, + kiss_bnfa_partial_state_size(comp_state->num_trans), + rname, + "partial" + ) != KISS_OK) { + return KISS_ERROR; + } + + // Fill in the transition number and fail state + kiss_bnfa_build_partial_state_header(&bnfa_state->partial, comp_state->num_trans, + state_bnfa_offset(comp_state->fail_state, TRUE)); + thinnfa_debug_extended(("%s: The fail state is %s\n", rname, state_name(comp_state->fail_state))); + + // Build a transition for each existing character + trans_num = 0; + for (child = comp_state->child; child != NULL; child = child->sibling) { + thinnfa_debug_extended(("%s: Setting the transition by %02x to %s\n", rname, + child->tran_char, state_name(child))); + bnfa_state->partial.transitions[trans_num].tran_char = child->tran_char; + bnfa_state->partial.transitions[trans_num].next_state_offset = + kiss_bnfa_offset_compress(state_bnfa_offset(child, FALSE)); + trans_num++; + } + KISS_ASSERT(trans_num == comp_state->num_trans, "%s: State %s should have %d transitions, but it has %d", + rname, state_name(comp_state), comp_state->num_trans, trans_num); + ENUM_SET_FLAG(comp_state->flags, THIN_NFA_STATE_BUILT_TABLE); + + return KISS_OK; +} + + +// Build a match state. +static kiss_ret_val +kiss_bnfa_build_match_state(kiss_thin_nfa_state_t *comp_state, u_int match_id) +{ + static const char rname[] = "kiss_bnfa_build_match_state"; + kiss_bnfa_offset_t match_bnfa_offset = state_bnfa_offset(comp_state, FALSE); + kiss_bnfa_state_t *match_state = comp_bnfa_offset_to_state(comp_state->comp, match_bnfa_offset); + kiss_bnfa_offset_t following_state_offset, real_state_offset; + + if (verify_add_state(comp_state, match_state, sizeof(kiss_bnfa_match_state_t), rname, "match") != KISS_OK) { + return KISS_ERROR; + } + + // Fill in the match state + match_state->match.type = KISS_BNFA_STATE_MATCH; + match_state->match.unused = 0; + match_state->match.match_id = match_id; + + // Add a jump state if the real state isn't directly following the match state (i.e. for full-matching states). + real_state_offset = state_bnfa_offset(comp_state, TRUE); + following_state_offset = match_bnfa_offset + sizeof(kiss_bnfa_match_state_t); + if (following_state_offset != real_state_offset) { + kiss_bnfa_state_t *jump_state = comp_bnfa_offset_to_state(comp_state->comp, following_state_offset); + + // Add a jump state (a 0-transition partial state) to the real state + if (verify_add_state(comp_state, jump_state, kiss_bnfa_partial_state_size(0), rname, "jump") != KISS_OK) { + return KISS_ERROR; + } + kiss_bnfa_build_partial_state_header(&jump_state->partial, 0, real_state_offset); + } + return KISS_OK; +} + + +// Encode a state in binary NFA form. +static kiss_ret_val +kiss_bnfa_add_state(kiss_thin_nfa_state_t *comp_state, u_int offset_in_pat_match_array) +{ + if (comp_state->flags & THIN_NFA_STATE_MATCH) { + // Build a match state (a jump state too if needed) + if (kiss_bnfa_build_match_state(comp_state, offset_in_pat_match_array) != KISS_OK) return KISS_ERROR; + } + + // Add the state + if (comp_state->flags & THIN_NFA_STATE_FULL) { + if (kiss_bnfa_build_full_state(comp_state) != KISS_OK) return KISS_ERROR; + } else { + if (kiss_bnfa_build_partial_state(comp_state) != KISS_OK) return KISS_ERROR; + } + + return KISS_OK; +} + +static uintptr_t +pat_key_hash_func(const void *key, CP_MAYBE_UNUSED void *info) +{ + const kiss_thin_nfa_pattern_array_t *pat_arr = (const kiss_thin_nfa_pattern_array_t *)key; + const char* buf = (const char *)key; + const char *buf_end; + uintptr_t val = 0; + + buf_end = buf + kiss_thin_nfa_pattern_array_size(pat_arr->n_patterns); + + for ( ; buf != buf_end; buf++) { + val = ((val >> 3) ^ (val<<5)) + *buf; + } + return val; +} + +static int +pat_key_cmp_func(const void *key1, const void *key2, CP_MAYBE_UNUSED void *info) +{ + const kiss_thin_nfa_pattern_array_t *pat1 = (const kiss_thin_nfa_pattern_array_t *)key1; + const kiss_thin_nfa_pattern_array_t *pat2 = (const kiss_thin_nfa_pattern_array_t *)key2; + + if (pat1->n_patterns != pat2->n_patterns) { + return 1; // No match + } + + return memcmp(pat1, pat2, kiss_thin_nfa_pattern_array_size(pat1->n_patterns)); +} + +static u_int +pattern_list_len(const kiss_thin_nfa_pattern_list_t *pat_list) +{ + const kiss_thin_nfa_pattern_list_t *pat; + u_int n = 0; + for (pat = pat_list; pat != NULL; pat = pat->next) { + n++; + } + return n; +} + +static kiss_ret_val +kiss_bnfa_match_patterns_prepare(struct thin_nfa_comp_s *nfa_comp, KissThinNFA *nfa) +{ + static const char rname[] = "kiss_bnfa_match_patterns_prepare"; + kiss_thin_nfa_pattern_array_t *pat_arr; + kiss_thin_nfa_state_t *comp_state; + u_int total_size_for_patterns; + + total_size_for_patterns = 0; + for (comp_state = nfa_comp->root_state; + comp_state != NULL; + comp_state = kiss_thin_nfa_get_subsequent_state(nfa_comp, comp_state)) { + if (comp_state->flags & THIN_NFA_STATE_MATCH) { + if (!comp_state->ids) { + thinnfa_debug_critical(( + "%s: State %s is finite, but its IDs are null\n", + rname, + state_name(comp_state) + )); + kiss_thin_nfa_set_comp_error(nfa_comp, "The state is finite, but its IDs are null"); + return KISS_ERROR; + } + total_size_for_patterns += kiss_thin_nfa_pattern_array_size(pattern_list_len(comp_state->ids)); + } + } + + if (total_size_for_patterns == 0) { + thinnfa_debug_critical(("%s: no finite states?!\n", rname)); + kiss_thin_nfa_set_comp_error(nfa_comp, "no finite states?!"); + return KISS_ERROR; + } + + // We allocate according to maximum possible size. + // We might reduce it at the end, if duplicates exist. + thinnfa_debug(("%s: alocating %u bytes for a pattern array\n", rname, total_size_for_patterns)); + pat_arr = (kiss_thin_nfa_pattern_array_t *)kiss_pmglob_memory_kmalloc_ex( + total_size_for_patterns, + rname, + FW_KMEM_SLEEP + ); + if (!pat_arr) { + thinnfa_debug_critical(( + "%s: failed to allocate %d bytes for a complete pattern array\n", + rname, + total_size_for_patterns + )); + kiss_thin_nfa_set_comp_error(nfa_comp, "Failed to allocate memory for a complete pattern array"); + return KISS_ERROR; + } + + nfa->pattern_arrays = pat_arr; + nfa->pattern_arrays_size = total_size_for_patterns; + + nfa_comp->patterns_hash = kiss_hash_create_with_ksleep( + nfa->match_state_num, + pat_key_hash_func, + pat_key_cmp_func, + NULL + ); + if (!nfa_comp->patterns_hash) { + thinnfa_debug(( + "%s: failed to create patterns hash table for %u finite states\n", + rname, + nfa->match_state_num + )); + kiss_thin_nfa_set_comp_error(nfa_comp, "Failed to create patterns hash table for finite states"); + return KISS_ERROR; + } + + return KISS_OK; +} + +static kiss_ret_val +kiss_bnfa_match_patterns_finalize(struct thin_nfa_comp_s *nfa_comp, KissThinNFA *nfa, u_int new_size) +{ + static const char rname[] = "kiss_bnfa_match_patterns_finalize"; + kiss_thin_nfa_pattern_array_t *new_pat_arr; + + // Compact the match patter array, if needed + if (new_size == nfa->pattern_arrays_size) { + thinnfa_debug(("%s: no size change - the pattern array size is %u bytes\n", rname, new_size)); + return KISS_OK; + } + + if (new_size > nfa->pattern_arrays_size) { + thinnfa_debug_critical(( + "%s: new pattern array size (%u) is greater than the current size (%u). This should not happen.\n", + rname, + new_size, + nfa->pattern_arrays_size + )); + kiss_thin_nfa_set_comp_error(nfa_comp, "Failed to allocate a complete pattern array"); + return KISS_ERROR; + } + + new_pat_arr = (kiss_thin_nfa_pattern_array_t *)kiss_pmglob_memory_kmalloc_ex(new_size, rname, FW_KMEM_SLEEP); + if (!new_pat_arr) { + thinnfa_debug_critical(("%s: failed to allocate %d bytes for a complete pattern array\n", rname, new_size)); + kiss_thin_nfa_set_comp_error(nfa_comp, "Failed to allocate a complete pattern array"); + return KISS_ERROR; + } + + thinnfa_debug(("%s: reducing the size from %u to %u\n", rname, nfa->pattern_arrays_size, new_size)); + bcopy(nfa->pattern_arrays, new_pat_arr, new_size); + kiss_pmglob_memory_kfree(nfa->pattern_arrays, nfa->pattern_arrays_size, rname); + nfa->pattern_arrays = new_pat_arr; + nfa->pattern_arrays_size = new_size; + return KISS_OK; +} + + +static kiss_ret_val +kiss_bnfa_copy_pat_list( + const KissThinNFA *nfa, + kiss_hash_t patterns_hash, + kiss_thin_nfa_state_t *comp_state, + u_int *last_used_offset_in_pat_match_array, + u_int *offset_for_cur_state) +{ + static const char rname[] = "kiss_bnfa_copy_pat_list"; + + if (comp_state->flags & THIN_NFA_STATE_MATCH) { + kiss_thin_nfa_pattern_list_t *pat_list_ent; + kiss_thin_nfa_pattern_array_t *pat_arr; + kiss_thin_nfa_pattern_array_t *cached_pat_arr; + u_int pat_arr_size; + u_int n_patterns; + u_int i; + + n_patterns = pattern_list_len(comp_state->ids); + pat_arr_size = kiss_thin_nfa_pattern_array_size(n_patterns); + + if ((*last_used_offset_in_pat_match_array + pat_arr_size) > nfa->pattern_arrays_size) { + thinnfa_debug_critical(("%s: offset (%u) + required size (%u) exceeds the total array size (%u)\n", + rname, *last_used_offset_in_pat_match_array, pat_arr_size, nfa->pattern_arrays_size)); + return KISS_ERROR; + } + + pat_arr = kiss_thin_nfa_offset_to_pat_array_ptr(nfa, *last_used_offset_in_pat_match_array); + pat_arr->n_patterns = n_patterns; + for (i = 0, pat_list_ent = comp_state->ids; i < pat_arr->n_patterns; pat_list_ent = pat_list_ent->next, i++) { + bcopy(&(pat_list_ent->pattern), &(pat_arr->pattern[i]), sizeof(pat_list_ent->pattern)); + } + + kiss_thin_nfa_free_pattern_ids(comp_state->ids); + comp_state->ids = NULL; // Prevent release when the state is cleaned up + + cached_pat_arr = (kiss_thin_nfa_pattern_array_t *)kiss_hash_lookkey(patterns_hash, pat_arr); + if (cached_pat_arr) { + u_int cached_offset; + cached_offset = kiss_thin_nfa_pat_array_ptr_to_offset(nfa, cached_pat_arr); + // No need to move the last_used_offset + *offset_for_cur_state = cached_offset; + thinnfa_debug(( + "%s: returning cached offset of %u for the state ID %u. " + "%u patterns %u bytes. The offset stays at %u.\n", + rname, + *offset_for_cur_state, + comp_state->state_id, + n_patterns, pat_arr_size, + *last_used_offset_in_pat_match_array + )); + } else { + *offset_for_cur_state = *last_used_offset_in_pat_match_array; + if (!kiss_hash_insert(patterns_hash, pat_arr, NULL)) { + thinnfa_debug(("%s: failed to insert a pattern into a hash (non-critical error)\n", rname)); + } + *last_used_offset_in_pat_match_array += pat_arr_size; + thinnfa_debug(( + "%s: returning the offset of %u for the state ID %u. %u patterns, %u bytes. The offset moved to %u.\n", + rname, + *offset_for_cur_state, + comp_state->state_id, + n_patterns, pat_arr_size, + *last_used_offset_in_pat_match_array + )); + } + } + + return KISS_OK; +} + + +static void +kiss_bnfa_update_state_depth(kiss_thin_nfa_state_t *comp_state) +{ + struct kiss_thin_nfa_depth_map_s *map = &comp_state->comp->runtime_nfa->depth_map; + u_char depth = MIN(comp_state->depth, KISS_THIN_NFA_MAX_ENCODABLE_DEPTH); + kiss_bnfa_offset_t off; + + // Update depth at the state's offset + off = comp_state->bnfa_offset; + map->offset0[kiss_bnfa_offset_compress(off)] = depth; + + // Matching state? Update at the match state offset too. + off = comp_state->bnfa_incoming_off; + if (off == comp_state->bnfa_offset) return; + map->offset0[kiss_bnfa_offset_compress(off)] = depth; + + // Full-matching state? Update at the jump state offset too. + off += sizeof(kiss_bnfa_match_state_t); + if (off == comp_state->bnfa_offset) return; + map->offset0[kiss_bnfa_offset_compress(off)] = depth; +} + + +// Based on thin_nfa_comp_s structure we have built, create a binary Thin NFA. +// Parameter: +// nfa_comp - the NFA's compilation data structure. +// +// Performance notes: +// This function takes most of the CPU time in the compilation process (in my tests, at least). +// Within it, time is divided about equally between full and partial states. +// Full states take about 40 times more time, but there are about 40 times more partial states. +// Overall, compilation time isn't bad, but there are surely optimization options. +// Idea - when constructing a full state, start by bcopy() of its fail state transitions. This would require +// filling the states in BFS order, which isn't done today. +static kiss_ret_val +kiss_bnfa_fill_states(struct thin_nfa_comp_s *nfa_comp) +{ + static const char rname[] = "kiss_bnfa_fill_states"; + KissThinNFA *nfa = nfa_comp->runtime_nfa.get(); + kiss_thin_nfa_state_t *comp_state; + u_int last_used_offset_in_pat_match_array = 0; + + thinnfa_debug(("%s: Filling BNFA %p size %d with %d states\n", rname, + nfa->bnfa_start, nfa->max_bnfa_offset-nfa->min_bnfa_offset, nfa_comp->state_num)); + + if (kiss_bnfa_match_patterns_prepare(nfa_comp, nfa) != KISS_OK) { + return KISS_ERROR; + } + + // Go over the states and build the BNFA representation + for (comp_state = nfa_comp->root_state; + comp_state != NULL; + comp_state = kiss_thin_nfa_get_subsequent_state(nfa_comp, comp_state)) { + u_int state_id = comp_state->state_id; + u_int offset_for_cur_state = (u_int)-1; + + if (kiss_bnfa_copy_pat_list( + nfa, nfa_comp->patterns_hash, comp_state, + &last_used_offset_in_pat_match_array, + &offset_for_cur_state + ) != KISS_OK) { + thinnfa_debug_critical(( + "%s: kiss_bnfa_copy_pat_list() failed for the state %s\n", + rname, + state_name(comp_state) + )); + kiss_thin_nfa_set_comp_error(nfa_comp, "kiss_bnfa_copy_pat_list() failed"); + return KISS_ERROR; + } + + // Update the maximum pattern length (length = state depth) + if (comp_state->depth > nfa->max_pat_len) { + nfa->max_pat_len = comp_state->depth; + } + + // Build the state + if (kiss_bnfa_add_state(comp_state, offset_for_cur_state) != KISS_OK) { + thinnfa_debug_critical(("%s: Failed to add the state %d\n", rname, state_id)); + return KISS_ERROR; + } + + // Update the depth map + kiss_bnfa_update_state_depth(comp_state); + } + + if (kiss_bnfa_match_patterns_finalize(nfa_comp, nfa, last_used_offset_in_pat_match_array) != KISS_OK) { + return KISS_ERROR; + } + + return KISS_OK; +} + + +static void +kiss_thin_nfa_fill_stats(struct thin_nfa_comp_s *nfa_comp) +{ + struct kiss_thin_nfa_specific_stats_s *stats = &nfa_comp->runtime_nfa->stats.specific; + + stats->num_of_states = nfa_comp->state_num; + stats->num_of_final_states = nfa_comp->match_state_num; +} + + +// Get the nfa_comp structure and build, according to it, the runtime Thin NFA structure. +static kiss_ret_val +kiss_thin_nfa_build_bnfa(struct thin_nfa_comp_s *nfa_comp, CP_MAYBE_UNUSED u_int compile_flags) +{ + static const char rname[] = "kiss_thin_nfa_build_bnfa"; + + thinnfa_debug_major(("%s: Converting the compiled Thin NFA to the binary form\n", rname)); + + // Get the list of all BNFA offsets + if (kiss_bnfa_calc_offsets(nfa_comp) != KISS_OK) { + thinnfa_debug_err(("%s: Error allocating the offset list\n", rname)); + kiss_thin_nfa_set_comp_error(nfa_comp, "Failed to allocate the offset list"); + return KISS_ERROR; + } + + // Allocate the runtime Thin NFA structure + nfa_comp->runtime_nfa = kiss_thin_nfa_create( + nfa_comp->match_state_num, + nfa_comp->min_bnfa_off, + nfa_comp->max_bnfa_off + ); + if (!nfa_comp->runtime_nfa) { + thinnfa_debug_err(("%s: Error creating the NFA\n", rname)); + kiss_thin_nfa_set_comp_error(nfa_comp, "Failed to allocate BNFA"); + return KISS_ERROR; + } + + if (nfa_comp->anchored_root_state) { + ENUM_SET_FLAG(nfa_comp->runtime_nfa->flags, KISS_THIN_NFA_HAS_ANCHOR); + } + + // Build the BNFA we'll use on runtime + if (kiss_bnfa_fill_states(nfa_comp) != KISS_OK) { + thinnfa_debug_err(("%s: kiss_bnfa_fill_states() failed\n", rname)); + return KISS_ERROR; + } + + // Copy the character translation table + if (nfa_comp->xlation_tab) { + bcopy( + nfa_comp->xlation_tab->tab, + nfa_comp->runtime_nfa->xlation_tab, + sizeof(nfa_comp->runtime_nfa->xlation_tab) + ); + ENUM_SET_FLAG(nfa_comp->runtime_nfa->flags, KISS_THIN_NFA_USE_CHAR_XLATION); + } + + kiss_thin_nfa_fill_stats(nfa_comp); + + thinnfa_debug_major(("%s: Created the binary Thin NFA %p\n", rname, nfa_comp->runtime_nfa.get())); + return KISS_OK; +} + +static void +kiss_thin_nfa_select_options(struct thin_nfa_comp_s *nfa_comp, CP_MAYBE_UNUSED u_int compile_flags) +{ + ENUM_SET_FLAG(nfa_comp->flags, THIN_NFA_ENABLE_ANCHOR_OPT); + nfa_comp->full_state_tier_num = kiss_thin_nfa_full_tiers_num; + ENUM_SET_FLAG(nfa_comp->flags, THIN_NFA_USE_RECURSIVE_COMPILE); + return; +} + + +// Compiling the SM according to Aho-Corasick algorithm. +// +// The DFA has two types of states: +// 1. Full states - have a transition for each possible character. +// 2. Partial states - only have transitions for characters that take us forward in some string. +// For all other characters, a "fail state" is defined, and the transition is what that state would have done. +// +// Paraemters: +// patterns - a set of string patterns which the resulting automaton would search for. +// compile_flags - flags with the KISS_PM_COMP_ prefix. +// error - output - on failure, would be set to indicate the reason. +// Retuns NULL on error, pointer to a newly allocated handle on success. +std::unique_ptr +kiss_thin_nfa_compile(const std::list &patterns, u_int compile_flags, KissPMError *error) +{ + static const char rname[] = "kiss_thin_nfa_compile"; + struct thin_nfa_comp_s *nfa_comp = NULL; + std::unique_ptr nfa; + + thinnfa_debug_major(("%s: Compiling a Thin NFA, flags=%x\n", rname, compile_flags)); + + // Creates a new kiss_thin_dfa_handle with initial state allocated + nfa_comp = kiss_thin_nfa_comp_create(error); + if (nfa_comp == NULL) { + thinnfa_debug_err(("%s: Failed to create a compile time structure\n", rname)); + kiss_pm_error_set_details(error, KISS_PM_ERROR_INTERNAL, "Failed to allocate the compilation information"); + goto finish; + } + + // Enable some optimization flags as needed + kiss_thin_nfa_select_options(nfa_comp, compile_flags); + + // Handle character translation - instead of converting to lowercase, build a translation + // tabel and use it when adding patterns to the trie and building transition tables. + if (kiss_thin_nfa_create_xlation_tab(nfa_comp, compile_flags) != KISS_OK) { + thinnfa_debug_err(("%s: Function kiss_thin_nfa_create_xlation_tab() failed\n", rname)); + goto finish; + } + + // Build a trie which contains all the pattern texts. + for (auto &pattern : patterns) { + // Adding each pattern to the the Thin NFA - Aho-Corasick first phase + if (kiss_thin_nfa_add_pattern_to_trie(nfa_comp, &pattern) != KISS_OK) { + thinnfa_debug_err(("%s: Function kiss_thin_nfa_add_pattern_to_trie() failed\n", rname)); + goto finish; + } + } + + // Calculate fail states for all NFA states + if (kiss_thin_nfa_calc_fail_states(nfa_comp) != KISS_OK) { + thinnfa_debug_err(("%s: Function kiss_thin_nfa_calc_fail_states() failed\n", rname)); + goto finish; + } + + // Convert the compilation data structure to the runtime structure + if (kiss_thin_nfa_build_bnfa(nfa_comp, compile_flags) != KISS_OK) { + thinnfa_debug_err(("%s: Function kiss_thin_nfa_build_bnfa() failed\n", rname)); + goto finish; + } + + if (!kiss_thin_nfa_is_valid(nfa_comp->runtime_nfa.get())) { + thinnfa_debug_err(("%s: Function kiss_thin_nfa_is_valid() failed\n", rname)); + goto finish; + } + + // Get the resulting NFA (set NULL to protect from free) + nfa = std::move(nfa_comp->runtime_nfa); + thinnfa_debug_major(("%s: Successfully compiled the Thin NFA %p\n", rname, nfa.get())); + +finish: + if (nfa_comp != NULL) { + // We destroy the compilation data structure, whether we succeed or fail. + kiss_thin_nfa_comp_destroy(nfa_comp); + } + return nfa; +} +SASAL_END diff --git a/components/utils/pm/kiss_thin_nfa_impl.h b/components/utils/pm/kiss_thin_nfa_impl.h new file mode 100644 index 0000000..81e0ea4 --- /dev/null +++ b/components/utils/pm/kiss_thin_nfa_impl.h @@ -0,0 +1,189 @@ +// Copyright (C) 2022 Check Point Software Technologies Ltd. All rights reserved. + +// Licensed under the Apache License, Version 2.0 (the "License"); +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef __h_kiss_thin_nfa_impl_h__ +#define __h_kiss_thin_nfa_impl_h__ + +// *********************** OVERVIEW ****************************** +// Thin NFA definitions, which are only used by Thin NFA files. +// 1. A list of patterns which is associated with a finite state. +// 2. APIs for building and destroying the Thin NFA structures. +// **************************************************************** + +#include +#include +#include + +#include "i_pm_scan.h" +#include "kiss_patterns.h" +#include "kiss_pm_stats.h" +#include "kiss_thin_nfa_base.h" + +KISS_ASSERT_COMPILE_TIME(KISS_PM_ALPHABET_SIZE == KISS_THIN_NFA_ALPHABET_SIZE); + +// Information we keep about a pattern +typedef struct { + int id; // PM Internal pattern ID + u_int pattern_id_flags; // KISS_PM_COMP_ prefix + u_int len; +} kiss_thin_nfa_pattern_t; + +// Linked list of pattern information - held per finite state, to indicate what it's accepting. +typedef struct kiss_thin_nfa_pattern_list_s { + struct kiss_thin_nfa_pattern_list_s *next; + kiss_thin_nfa_pattern_t pattern; +} kiss_thin_nfa_pattern_list_t; + +// Array of pattern information - offset to it held per finite state, to indicate what it's accepting. +typedef struct kiss_thin_nfa_pattern_array_s { + u_int n_patterns; + // NOTE! Always keep this last! + kiss_thin_nfa_pattern_t pattern[1]; // Dynamic array, not really 1 + // Do NOT add anything here! +} kiss_thin_nfa_pattern_array_t; + +static CP_INLINE u_int +kiss_thin_nfa_pattern_array_size(const u_int n_patterns) +{ + // assignement of NULL value so Windows compiler won't cry about unused variable. + kiss_thin_nfa_pattern_array_t CP_MAYBE_UNUSED *dummy = NULL; + + // We substract sizeof(->pattern), becuase it's already included in the sizeof + // of the whole struct. + return (sizeof(*dummy) + n_patterns * sizeof(dummy->pattern[0]) - sizeof(dummy->pattern));; +} + +// ThinNFA statistics + +// Specific ThinNFA Statistics +struct kiss_thin_nfa_specific_stats_s { + u_int num_of_states; // number of states in this thin_nfa + u_int num_of_final_states; // number of final states in this thin_nfa +}; + +// Statistics for ThinNFA +struct kiss_thin_nfa_stats_s { + struct kiss_pm_stats_common_s common; // Run-time (per-CPU, dynamic) and build-time common statistics + struct kiss_thin_nfa_specific_stats_s specific; // Build-time specific ThinNFA statistics +}; +typedef struct kiss_thin_nfa_stats_s *kiss_thin_nfa_stats; + +// Compressed BNFA offset -> state depth map +struct kiss_thin_nfa_depth_map_s { + u_char *mem_start; // Array of depth per BNFA compressed offset + u_int size; + u_char *offset0; // Positive/negative offsets are relative to this +}; + +#define KISS_THIN_NFA_MAX_ENCODABLE_DEPTH 255 // Fit in u_char + +// A Compiled Thin NFA, used at runtime +class KissThinNFA { +public: + ~KissThinNFA(); + + kiss_bnfa_state_t *bnfa_start; // The first (in memory) and initial state + kiss_bnfa_state_t *bnfa; // The state at offset 0 (somewhere in the middle) + kiss_bnfa_offset_t min_bnfa_offset; // The offset of the first (and initial) state. + kiss_bnfa_offset_t max_bnfa_offset; // The offset after the last state. + enum kiss_thin_nfa_flags_e flags; + u_int match_state_num; // Number of match states in the machine + u_int pattern_arrays_size; // Total size in bytes of concatanated pattern arrays + kiss_thin_nfa_pattern_array_t *pattern_arrays; // A pointer to a buffer holding ALL pattern arrays, for ALL states + struct kiss_thin_nfa_stats_s stats; + u_int max_pat_len; // Length of the longest string + u_char xlation_tab[KISS_PM_ALPHABET_SIZE]; // For caseless/digitless + struct kiss_thin_nfa_depth_map_s depth_map; // State -> Depth mapping +}; + +static CP_INLINE u_int +kiss_thin_nfa_pat_array_ptr_to_offset(const KissThinNFA *nfa, const kiss_thin_nfa_pattern_array_t *pat_arr) +{ + return (const char *)pat_arr - (const char *)(nfa->pattern_arrays); +} + +static CP_INLINE kiss_thin_nfa_pattern_array_t * +kiss_thin_nfa_offset_to_pat_array_ptr(const KissThinNFA *nfa, const u_int offset) +{ + return (kiss_thin_nfa_pattern_array_t *)((char *)(nfa->pattern_arrays) + offset); +} + +// Get a state's depth +// For very deep states (offset >= 255), returns the maximum pattern length, +// which would be greater/equal the real state depth. +static CP_INLINE u_int +kiss_bnfa_offset_to_depth(const KissThinNFA *nfa, kiss_bnfa_comp_offset_t comp_offset) +{ + u_int depth = nfa->depth_map.offset0[comp_offset]; + return (depth==KISS_THIN_NFA_MAX_ENCODABLE_DEPTH) ? nfa->max_pat_len : depth; +} + + +// Create a new empty Thin NFA. +// Allocates the BNFA and the match_data array, but doesn't fill them. +std::unique_ptr +kiss_thin_nfa_create( + u_int match_state_num, + kiss_bnfa_offset_t min_offset, + kiss_bnfa_offset_t max_offset +); + + +// Add a pattern (with given id, flags and length) to a list. +// pat_list should point to the head of the list, *pat_list may be modified. +kiss_ret_val +kiss_thin_nfa_add_pattern_id( + kiss_thin_nfa_pattern_list_t **pat_list, + const kiss_thin_nfa_pattern_t *pat_info +); + +// Free all patterns on a list. +void kiss_thin_nfa_free_pattern_ids(kiss_thin_nfa_pattern_list_t *pat_list); + +// Compile a Thin NFA +std::unique_ptr +kiss_thin_nfa_compile( + const std::list &patterns, + u_int compile_flags, + KissPMError *error +); + + +// Validate Thin NFA +BOOL kiss_thin_nfa_is_valid(const KissThinNFA *nfa_h); + +void +kiss_thin_nfa_exec(KissThinNFA *nfa_h, const Buffer &buffer, std::vector> &matches); + +// Dump a PM +kiss_ret_val kiss_thin_nfa_dump(const KissThinNFA *nfa_h, enum kiss_pm_dump_format_e format); + +// Debugging macro wrappers. +// All get a format string plus parameters in double parenthesis: +// thinnfa_debug(("%s: hello, world\n", rname)); +// Meaning of each macro: +// thinnfa_debug_critical - Critical error, printed by default. +// thinnfa_debug_err - Error we should live with (e.g. usage error, memory allocation), not printed by default. +// thinnfa_debug - Normal debug messages. +// thinnfa_debug_major - Debug messages about several major events in Thin NFA constuction. Use sparingly. +// thinnfa_debug_extended - Low level debug messages, which may be printed in large numbers. +// thinnfa_dbg - An "if" statement checking the debug flag (equivalent to thinnfa_debug). +#define thinnfa_debug_critical(_str) kiss_debug_err(K_ERROR, _str) +#define thinnfa_debug_err(_str) kiss_debug_err(K_THINNFA|K_PM, _str) +#define thinnfa_debug(_str) kiss_debug_info(K_THINNFA, _str) +#define thinnfa_debug_major(_str) kiss_debug_info(K_THINNFA|K_PM, _str) +#define thinnfa_debug_extended(_str) kiss_debug_info(K_THINNFA, _str) +#define thinnfa_debug_perf(_str) kiss_debug_info_perf(K_THINNFA, _str) +#define thinnfa_dbg() kiss_dbg(K_THINNFA) + +#endif // __h_kiss_thin_nfa_impl_h__ diff --git a/components/utils/pm/lss_example.txt b/components/utils/pm/lss_example.txt new file mode 100644 index 0000000..28722c0 --- /dev/null +++ b/components/utils/pm/lss_example.txt @@ -0,0 +1,13760 @@ +# {\rt +^7b5c7274 +# RIFF +^52494646 +# /JBIG2Decode +2f4a424947324465636f6465 +# %PDF +^25504446 +# \xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1 +^d0cf11e0a1b11ae1 +# %pdf- +257064662d +# 0&\xb2u\x8ef\xcf\x11\xa6\xd9\x00\xaa\x00b\xcel +^3026b2758e66cf11a6d900aa0062ce6c +# strlstrh +7374726c73747268 +# RIFF +^52494646 +# BM +^424d +# BM +^424d +# \xffWPC +^ff575043 +# C$/Documents +43242f446f63756d656e7473 +# \x0039vmpami\x18\x00\x00\x00\x01\x00\x00\x00,\x00\x00\x00 +003339766d70616d6918000000010000002c000000 +# \x06rcl\x03l\x02\x0c\x95 +0672636c036c020c95 +# c\x01wna\x9cr\x00l\x00c\x01g\xc1r\x01 +6301776e619c72006c00630167c17201 +# 39vm +3339766d +# language.eng +6c616e67756167652e656e67 +# .pdf +2e706466 +# PK\x03\x04 +504b0304 +# send_one() +73656e645f6f6e652829 +# m_obj=new activexobject( +6d5f6f626a3d6e657720616374697665786f626a65637428 +# if (m_obj==null) +696620286d5f6f626a3d3d6e756c6c29 +# onstart="document. +6f6e73746172743d22646f63756d656e742e +# %PDF- +255044462d +# history.go( +686973746f72792e676f28 +# 2-2D05CB +322d324430354342 +# EMF +20454d46 +# document.all(i).setcapture() +646f63756d656e742e616c6c2869292e736574636170747572652829 +# avi +61766920 +# LIST\xa0\x84\x01\x00movi00dc\xe3\x02\x00\x00\xd0\xd8 +4c495354a08401006d6f766930306463e3020000d0d8 +# LIST\x90\x10\x00\x00strlstrh8\x00\x00\x00vidsMJPG +4c495354901000007374726c7374726838000000766964734d4a5047 +# RIFF +^52494646 +# -moz-column- +2d6d6f7a2d636f6c756d6e2d +# -moz-column- +2d6d6f7a2d636f6c756d6e2d +# -moz-column- +2d6d6f7a2d636f6c756d6e2d +# LsCM +4c73434d +# XFIR +^58464952 +# \x0aH\x89\xecU{LSi\x16 +0a4889ec557b4c536916 +# B-5DCCC7 +422d354443434337 +# 7-DB92E7 +372d444239324537 +# F-51EBEA +462d353145424541 +# 2-5DE3D1 +322d354445334431 +# var woo=0.4444444444444444444444444444444444444444444444444444444444444444444 +76617220776f6f3d302e34343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434343434 +# var pi = 3.1415926535897932384626433832795028841971693993751058209749445923078164 +766172207069203d20332e31343135393236353335383937393332333834363236343333383332373935303238383431393731363933393933373531303538323039373439343435393233303738313634 +# MM\x00* +^4d4d002a +# II*\x00 +^49492a00 +# 6F063-00 +36463036332d3030 +# 8-006097 +382d303036303937 +# 8-006097 +382d303036303937 +# C-312CA8 +432d333132434138 +# catch (e) {} ",701 +636174636820286529207b7d20222c373031 +# setInterval("try +736574496e74657276616c2822747279 +# setInterval(' +736574496e74657276616c2827 +# xdomainaccess() +78646f6d61696e6163636573732829 +# row.clearAttributes(); +726f772e636c6561724174747269627574657328293b +# tr.clearAttributes(); +74722e636c6561724174747269627574657328293b +# tHeader("Content-Length" +744865616465722822436f6e74656e742d4c656e67746822 +# estHeader("Referer" +65737448656164657228225265666572657222 +# estHeader("Host" +6573744865616465722822486f737422 +# 3-1AA39B +332d314141333942 +# F-750656 +462d373530363536 +# c-ed11e1 +632d656431316531 +# 7-79F01D +372d373946303144 +# flashObject +666c6173684f626a656374 +# flashObject +666c6173684f626a656374 +# jp2c\xffO\xffQ +6a703263ff4fff51 +# /jpxdecode +2f6a70786465636f6465 +# %pdf +^25706466 +# P\x00P\x004\x000\x00\x00\x00\x00\x00 +500050003400300000000000 +# level\x0dFifth level +6c6576656c0d4669667468206c6576656c +# LsCM +4c73434d +# XFIR +^58464952 +# var pi=3+0.14159265 +7661722070693d332b302e3134313539323635 +# "31337" + 0.31337313 +22333133333722202b20302e3331333337333133 +# +3c656d62656420747970653d27617564696f2f6d696469273e +# setTimeout(go, 1); +73657454696d656f757428676f2c2031293b +# asMimeTypes.shift +61734d696d6554797065732e7368696674 +# CollectGarbage +436f6c6c65637447617262616765 +# document.appendChild +646f63756d656e742e617070656e644368696c64 +# %PDF- +255044462d +# window.pkcs11.addmodule( +77696e646f772e706b637331312e6164646d6f64756c6528 +# this.points = f2() +746869732e706f696e7473203d2066322829 +# var aa = a[2].split('-') +766172206161203d20615b325d2e73706c697428272d2729 +# var a = arrelement.split('/') +7661722061203d20617272656c656d656e742e73706c697428272f2729 +# function f1 ( arrelement ) +66756e6374696f6e206631202820617272656c656d656e742029 +# Gs.push ( new G( Cs[i] )) +47732e707573682028206e65772047282043735b695d202929 +# var cs = [ '|h|3s', 'o|h|0,s', 'm|a|,15,','jn|a|' ] +766172206373203d205b20277c687c3373272c20276f7c687c302c73272c20276d7c617c2c31352c272c276a6e7c617c27205d +# this.os = f() +746869732e6f73203d20662829 +# function G ( d ) +66756e6374696f6e2047202820642029 +# 'Quantum|H|0,25,0,0,23 +275175616e74756d7c487c302c32352c302c302c3233 +# 'Pwnstar|H|26,12,20,9 +2750776e737461727c487c32362c31322c32302c39 +# function guild ( initData ) +66756e6374696f6e206775696c64202820696e6974446174612029 +# // Code by: Azka +2f2f20436f64652062793a20417a6b61 +# function set_timers +66756e6374696f6e207365745f74696d657273 +# <RecpA4><item> +2623363052656370413426233632262336306974656d26233632 +# +3c5265637041343e3c6974656d3e +# MM\x00* +^4d4d002a +# II*\x00 +^49492a00 +# \x03\xaf\x1e\xbe}\xeb\x06w\xed\xf5\xc4\xeb=\x8a\xbe\xdf +03af1ebe7deb0677edf5c4eb3d8abedf +# \x9fh\x9b\x0c\xa7.xo$\xf2\xe6\xd4i\x8f\xf3v +9f689b0ca72e786f24f2e6d4698ff376 +# \xfa\xa6a\xe3\xe7e\x83\x1a\xfd\xaf:\x9e7[]\xff +faa661e3e765831afdaf3a9e375b5dff +# PK\x03\x04\x14\x00\x06\x00\x08\x00\x00\x00!\x00\x89^ +504b030414000600080000002100895e +# shell: +7368656c6c3a +# testnode.onreadystatechange = function () +746573746e6f64652e6f6e726561647973746174656368616e6765203d2066756e6374696f6e202829 +# style.quotes +7374796c652e71756f746573 +# .applyElement +2e6170706c79456c656d656e74 +# scen_num\x1e\x01\x00 +7363656e5f6e756d1e0100 +# \x09\x00\x04\x00\x07\x00\x10\x00\x0b\x02\x0c +^09000400070010000b020c +# \xd7\xcd\xc6\x9a\x00\x00 +^d7cdc69a0000 +# reporttest(rep) {\x0d\x0avar +7265706f7274746573742872657029207b0d0a76617220 +# &&\x0d\x0ae.indexof('s +2026260d0a652e696e6465786f66282773 +# \x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a +2e646c6c222c20310d0a656e64207375620d0a3c2f7363726970743e0d0a3c2f626f64793e0d0a3c2f68746d6c3e0d0a +# ()">\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a \x0d\x0a +2e646c6c222c20310d0a656e64207375620d0a3c2f7363726970743e0d0a3c2f626f64793e0d0a3c2f68746d6c3e200d0a +# ()">\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a \x0d\x0a +2e646c6c222c20310d0a656e64207375620d0a3c2f7363726970743e0d0a3c2f626f64793e0d0a3c2f68746d6c3e20202020200d0a +# ()">\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a\x0d\x0a

\x0a +3c70207374796c653d22666c6f61743a6c656674223e3c2f703e0a2020202020202020202020203c61207374796c653d22666c6f61743a6c656674223e3c2f613e +# zoom:3000%;\x0d\x0a } +7a6f6f6d3a33303030253b0d0a207d +# \x1f\xa0\xab\xcd\xff\xff\xff\xff\xff\xff +^1fa0abcdffffffffffff +# \x9c\xcb\xcb\x8d\x13u\xd2\x11\x91X\x00\xc0OyV\xa4 +^9ccbcb8d1375d211915800c04f7956a4 +# \x01\x00\x09\x00\x00\x03\x11\x00\x00\x00\x00\x00\x05\x00\x00\x00\x00\x00\xff\xff\xff\xff\x13\x022\x00\x96\x00\x03\x00\x00\x00\x00\x00 +010009000003110000000000050000000000ffffffff130232009600030000000000 +# 5c6a8bcea23c6afc538370b457dea04f4c421164f946a1f7d79d0f97aeff0b07798c39615117c420346b82e99d +356336613862636561323363366166633533383337306234353764656130346634633432313136346639343661316637643739643066393761656666306230373739386333393631353131376334323033343662383265393964 +# 2c5c5c55749918f57e1af047aef8368fdf450f9a5cec7fd7bf209a60ef8b5f82dc7701b0c7d3c267590033520f +326335633563353537343939313866353765316166303437616566383336386664663435306639613563656337666437626632303961363065663862356638326463373730316230633764336332363735393030333335323066 +# \xc4\x10\x89_\x04\x8dV\x0c\x83\xc8\xff\xf0\x0f\xc1\x02H\x85\xc0\x7f\x0a\x8b\x0e\x8b\x11\x8bB\x04V\xff\xd0\x8bL$\x10_^\x89)][Y\xc2\x04\x00\xcc\xcc\xcc\xcc\xcc\xcc\xcch\x0e\x00\x07\x80\xe8\xb6\x01\x00\ +c410895f048d560c83c8fff00fc1024885c07f0a8b0e8b118b420456ffd08b4c24105f5e89295d5b59c20400cccccccccccccc680e000780e8b6010000cccccccccccc8b542404568bf18b068b48f083e8103950087d1585d27e +# \x00\x00\x89\x8d\xb8\xbb\xff\xff\xc7\x85\xb8\xbb\xff\xff8\x18\x06\x10j\x00\x8b\x8d\xb8\xbb\xff\xff\xe8k\x12\x00\x00\xc7E\xfc\x00\x00\x00\x00\x8b\x85\xb8\xbb\xff\xff\xc7\x00\xbci\x02\x10\xc7\x85\xe4\x +0000898db8bbffffc785b8bbffff381806106a008b8db8bbffffe86b120000c745fc000000008b85b8bbffffc700bc690210c785e4bbffff512fa20168fc3f00006a008d8de8bbffff51e834f5000083c40c68040100008d95e4 +# \xec3\x09\xd2\x0e\x0f\x84\x18\x89#\x04<\x0d\xbc\x10\x81\x8c\xde\xe3\xe6\x11\x09=\xaf\xae\xdd\xe9eE\x0f\xcea\xf8\x0fh,,Z\xe0<\xba\x16\x09\xb24D\x15\xe9 Ya1\x97\x83!l2\xf8\xaa$\xb1p_8\xf4\xb8\xbe\xa8Or +ec3309d20e0f84188923043c0dbc10818cdee3e611093dafaedde965450fce61f80f682c2c5ae03cba1609b2344415e9205961319783216c32f8aa24b1705f38f4b8bea84f726fe983be73de77e39e53a46907580e910a314e72 +# \xd6\xeedh\xce\xc3{\xd8-\x8f8y\xda\xe9\xa0\xf8\x05?x\x8c\xe3\x9c\x9c\xf3m\x1a(\xce\xcd9\xb1'\xb1\xd5\x02X-\x0a\x0b\x8b(\x0as\x09\xb4\x87r\xcfy\xb7\xe6\x82F-\xe0\\x13\xe6\x06MX\xd8\x94\x18\xc1"\xc5:@\ +d6ee6468cec37bd82d8f3879dae9a0f8053f788ce39c9cf36d1a28cecd39b127b1d502582d0a0b8b280a7309b48772cf79b7e682462de05c13e6064d58d89418c122c53a4003258006dae95a6038d6c5e83a38dd629ed531b46e +# \x04\xd0fC\x15\xd2\xff\xff\xd3\xd0fD\xa0t\xd7\xd3\xd0fE\x0f\x04\x00\x00\x10\x04\x00\x00\x10\xb1\xff\xff\xd2H\x00\x00\x06\x03\x08\x0a\x0b\xe2\x02\xd00 \x82c\x07 \x82c\x06 \x82c\x05 \x82c\x04 \x82c\x03 +04d0664315d2ffffd3d06644a074d7d3d066450f0400001004000010b1ffffd24800000603080a0be202d03020826307208263062082630520826304208263032082630220800563042400630521826306d1603eb312080000d0 +# f ,2'O!\x02`"f ,4`\x0df\x1cf\x1da\x1e`"f ,4'O!\x02`\x18f\x19`\x08F$\x01\x125\x00\x00`\x18f\x19`\x08F%\x01`\x15\x87\x80\x15\xd6\xd0\xd2B\x00`\x03\x87h\x14]&\xd0f\x14O&\x01]'O'\x00]\x0f`\x10f(\xd0f +66202c32274f2102602266202c34600d661c661d611e602266202c34274f2102601866196008462401123500006018661960084625016015878015d6d0d2420060038768145d26d066144f26015d274f27005d0f60106628d066 +# fa78cdc5b7776e3ffdee01b9e960b61bc3ae2df5e073d7618f13157382531f0c6ff90dcbcdabd66d4b3f01c339 +666137386364633562373737366533666664656530316239653936306236316263336165326466356530373364373631386631333135373338323533316630633666663930646362636461626436366434623366303163333339 +# ca78e3b8b53acbc21c6f40966ee3ae2a52f3cb7b17332c127a4b577109d2ded482366a4d530eaf5ea07d57a23d +636137386533623862353361636263323163366634303936366565336165326135326633636237623137333332633132376134623537373130396432646564343832333636613464353330656166356561303764353761323364 +# x\x82hl\x84-\xac\xbd\x8bU\xf4\xcc\xfe\xfd\xa7\xdfot\xbdt`\xbf\x18\xd9/>\xa57A\xb1\xd3>\xf8\x17\x9cg\xae\xe6\x0a\x85\xfc\x17R\xfe\x0b\xa8\xf0I(\xb6\xe4\x0b\xff\x02\xe7\xb2\xb8\xbc\x0aendstream \x0aend +7882686c842dacbd8b55f4ccfefda7df6f74bd7460bf18d92f3ea53741b1d33ef8179c67aee60a85fc1752fe0ba8f04928b6e40bff02e7b2b8bc0a656e6473747265616d200a656e646f626a200a322030206f626a200a3c3c0a +# E\xa6\xc9\x8d,?\x93\x8c\x91c\xe4\x062@\xb6\x92\xe7\xb2q\xec\xad\x90\x87\x93\xe7rrE\x85vg\x1c\xa3\xa4F\xae&\xaf&}\xe4\x1e\x16\xa7R\x1dTr=\xbb\xce\xc9\xa7\x81\xace\xf17\x91\x8dd\x90\x1c!\xafb\xf1W\x91Q +45a6c98d2c3f938c9163e4063240b692e7b271ecad908793e77272458576671ca3a446ae26af267de41e16a7521d54723dbbcec9a781ac65f137918d64901c21af62f15791517288bc9c94a49bec67fd9df61e969f93f77696ef +# \x94\xcbAr\xc35`,\xe1\x1b\xa4U\x16\xc8\x0f8Ud+\x94\xa3YGm#\xe2W\xe2\x89,$Z[\x13@\x0cTR\xaa1\x94p\x01H\xb0dm\x14\x99X!\x9c\xa2\x1c\xc4\xc2A)\x14\xb8Jg\x12\x1exY9pH\xf9\xe4\xc37q\x7f\xb7gI\xe6:X+\xa2\xf6\x8a\x92v#\xeaa\x99\x02\xaf\x14 +aa807ed2f16cc431d595e3afbb7264b55f5a4ded97be8cdabff9b2fe3b580863b44f718afc3eb84a67121e7859397048f9e4c337717fb76749e63a582ba2f68a927623ea619902af14c2ce1d6f2085c0e542978b88095ee9ccb96b095e8938bf7a7d5d +# \xdfQ\x08\xa57\x12\x13t\xb8/7\xcd,\xd3R\xcd\xcd\x03\xb3\x1c\x94\xdbC\xc0N\x8c\xc7>\x15\xe3\xf6\;\xc4\x8d\x04\x85\xb8\x18\x94\xbc\xdaAu\xc7\x11\x02o`\xff`\xf9I\x80\xf8\x0a\xf2\xbe\xd3\x9c"\xf0]S,\xd5T +df5108a537121374b82f37cd2cd352cdcd03b31c94db43c04e8cc73e15e3f65c3bc48d0485b81894bcda4175c711026f60ff60f94980f80af2bed39c22f05d532cd55417e325a59ca27f541a86d2a723bb6fa55a6600733152e2ab05d2f95f2b849892 +# CWS\x08\xdc\x1f\x00\x00x\xda\x94\x18kw\x1b\xd5q\xb4\xbb\x92\xae\xe5\xbcp\x0c\xca\xc3\x09\xa6P\x12\xb7!\x84\x10\x1a\xe28\x8am9vT\x9cU\x90Mh\x9a\x06w-\xaf\xa4\x0d\x92V]\xad\x83]\xe8\x8b6@\xa0\x0dm)"\xe +43575308dc1f000078da94186b771bd571b4bb92aee5bc700ccac309a65012b72184101ae2388a6d3976549c55904d689a06772dafa40d92565dad835de88b3640a00d6d2922e11128ef67dbd3d29efe897eea39eda79ec3a79e9e9ed3dfe0ceccdd5d +# A@h\xb2\x87\xfel\x91\xc3#|\x9dC\x05k\x0c\x93\xbd2\x9d\xb6\x06C\xb6/I\x86\x89^\xe16x\xf0\xba.\xa8\xc0c\x03'\xd1m\xa5\xa5V\xb0\xb7D +414068b287fe6c91c3237c9d43056b0c93bd329db60643b62f4986895ee13678f0ba2ea8c0630327d16da5a556b0b744 +# \xfd\x98+!u\xcdo6\x85b\xb8^2c\xc1\xb6v=\xcb#+)Z\xf1\xae\x1d\x13\xb6\\xe9\xab\x1d\xa1\x02\x12\x8f\x846\xbbv\xbe\xdfg\x8f\xa4\x95\x7f +fd982b2175cd6f368562b85e3263c1b6763dcb232b295af1ae1d13b65ce9ab1da102128f8436bb76bedf678fa4957f20 +# [\x0f6\xa5@\xb1\xe5\xfa\xb2\xdd\xdf\xe9J\xf5\x85\xdd\xf5h\xc9\xad\xb8\xbe\x1b\xdd\xa0\xc0Ry\x83\xb9\xd9\x8e#w\xa3\x90)\x0a\x9c,\x8d&W\xfe\x9a\xa0\xf5\x1f +5b0f36a540b1e5fab2dddfe94af585ddf568c9adb8be1bdda0c0527983b9d98e2377a390290a9c2c8d2657fe9aa0f51f +# \xf1\xdf\xe5\xd8\xe1\xae\xeb\x07jC\xaa\x90\xe4\x03\xa2fb\xb7\xb7\x87\xec\x03j\x15\xecJ\x15=i\xfa[\x81\xc0\xad1R\x87b%`W\x86\xcb\xe5ob{\xf8\xbdW]S\xee\x9eT\xc3 +f1dfe5d8e1aeeb076a43aa90e403a26662b7b787ec036a15ec4a153d69fa5b81c0ad3152876225605786cbe56f627bf8bd575d53ee9e54c3 +# \x09\x0d\xc5\x89\xa9[\xd7\xbd\x182\x14\x1e:\xb2\xb21\x0a\x8a\x14Vr\x86\xf2\x87x\xe1\xa1\xb43m\x06f\xe0\x91\x19~\x14\xc3YK$\x8e\xec\x0e\xe8A\xda\xfdt\xbe\xf3\x9d\xef\xb2\xfb\xd7\xdf\xbf\xfe\x0e\xa0\x8 +090dc589a95bd7bd1832141e3ab2b2310a8a14567286f28778e1a1b4336d0666e091197e14c3594b248eec0ee841dafd74bef39defb2fbd7dfbffe0ea0827b26dec707063e +# .dat\x0b.H,\xcf\xb35\xe2\xf2 +2e6461740b2e482ccfb335e2f2 +# 9.\xe6i\xcf\xf2\x88\xbe\x04X4e\x92E\xba\x95\xe9dt\x9e[<\x05;!\x85\x12\x90L\xc5F\x8dd\xe9O\xe8G\x82&\xe9\)$>#i\x1d# +392ee669cff288be045834659245ba95e964749e5b3c053b218512904cc5468d64e94fe8478226e95c29243e23691d23 +# mB;M\xdf\x19\x9c\xefQ'h\x12!^\x0c\x05\x94i/P\xa1\xe8\x13[\x09qfX@\x85\xf6"\xb3\xf8?\x01\xd7\x07\x04\x08\xbc\xdb\xcfm\xba\x97\x9b\x95 +6d423b4ddf199cef51276812215e0c0594692f50a1e8135b097166584085f622b3f83f01d7070408bcdbcf6dba979b95 +# \x0b\xb7m\xdc\xc9\xe3.\xee\xd9\xb8\x9f\xc7\x12\x96\x05\xce%>;\xd5\xd8\xd3~\xcbT\xddtY\x09\xc9O\x9e\x8ey\xee\xc7\x15\x13u\x0bQ\xb8Ux\xd6\x0e\xd3\x8e +0bb76ddcc9e32eeed9b89fc7129605ce253e3bd5d8d37ecb54dd745909c94f9e8e79eec71513750b51b85578d60ed38e +# \xc1 /G\xb4\xf3Y\xaa\xb4D\x15\x96)q\x053\x14=M\xbfC\x89\xb6k\x96n\xa0H\xed\xb9\x8eYmO\xc7EN\xa5]\x8a{\xba\xfc\x13PK\x07\x08 +c1202f47b4f359aab444159629710533143d4dbf4389b66b966ea048edb98e596d4fc7454ea55d8a7bbafc13504b0708 +# \x94i/P\xa1\xe8\x13[\x09qfX@\x85\xf6"\xb3\xf8?\x01\xd7\x07\x04\x08\xbc\xdb\xcfm\xba\x97\x9b\x95\x1df\xbeI\xfb\xde\x11\xfc\x16i/\xb1\xaaC~\x07 +94692f50a1e8135b097166584085f622b3f83f01d7070408bcdbcf6dba979b951d66be49fbde11fc16692fb1aa437e07 +# \x93\xd3%\xdf\x90\xdb\x91~J\xf9L\xac\x87\xf8\xe1\x00rb\xbb\xbe\xe5-E\xcdV\xe0\xcb\xd0\xa4\x85}-Kj\x15\x9aZ\xd4h(-p\xbeX\x1aP\x11\x10 +93d325df90db917e4af94cac87f8e1007262bbbee52d45cd56e0cbd0a4857d2d4b6a159a5ad468282d70be581a501110 +# \x1e\x9dV\xa3A\xed\x1a\xce\xdc\xb7M\xd3&hg\x1e\x94\x1f\x8e\x1c\xa1\xdb\xd1N\xbd@\x961\xeb\x0boG6;\xaf\xa8\x851\xad\x08}\x17\xf3\xe0\xf0\xffxC +1e9d56a341ed1acedcb74dd32668671e941f8e1ca1dbd14ebd409631eb0b6f47363bafa88531ad087d17f3e0f0ff7843 +# * \x9d\x16\xc3r\xa1>\xaa\xb6Z<\x07\x0d\x1e\x80\x12\xd3m\xa9\x1a\xd2\xec\xf9\x96\xda\x1f\x0c\xcf\x90+\x0c\xa5\x0c\x87\xab#M^7w\xa5\xa9\xfe\xd9DS\xee +2a209d16c372a13eaab65a3c070d1e8012d36da91ad2ecf996da1f0ccf902b0ca50c87ab234d5e3777a5a9fed94453ee +# \x829\xc5d\xb2g\xf98\xe0\x8a\x90\xe0\xb5&a\x08H\xd8\xack\xc9%\xd3\x94\xda\x0e\x1c\x1e\xef&sj+T\xd6\xac\xd6\x0d>~\xe7\x80\x0c\x97\e\x013 +8239c564b267f938e08a90e0b526610848d8ac6bc925d394da0e1c1eef26736a2b54d6acd60d3e7ee7800c975c650133 +# FWS +^465753 +# 3F8A6C33-E +33463841364333332d45 +# 75-11D1-A3 +37352d313144312d4133 +# 5D08B586-3 +35443038423538362d33 +# D2D588B5-D +44324435383842352d44 +# 00022613-0 +30303032323631332d30 +# 67DCC487-A +36374443433438372d41 +# 466D66FA-9 +34363644363646412d39 +# ECABB0BF-7 +45434142423042462d37 +# B4B3AECB-D +42344233414543422d44 +# E846F0A0-D +45383436463041302d44 +# 85BBD920-4 +38354242443932302d34 +# C7B6C04A-C +43374236433034412d43 +# EEED4C20-7 +45454544344332302d37 +# D99F7670-7 +44393946373637302d37 +# B0516FF0-7 +42303531364646302d37 +# 9478F640-7 +39343738463634302d37 +# 860D28D0-8 +38363044323844302d38 +# 6D36CE10-7 +36443336434531302d37 +# 510A4910-7 +35313041343931302d37 +# 2A6EB050-7 +32413645423035302d37 +# 8EE42293-C +38454534323239332d43 +# 3050F391-9 +33303530463339312d39 +# FBEB8A05-B +46424542384130352d42 +# 7849596A-4 +37383439353936412d34 +# AF604EFE-8 +41463630344546452d38 +# 01E04581-4 +30314530343538312d34 +# 52CA3BCF-3 +35324341334243462d33 +# FD78D554-4 +46443738443535342d34 +# D2923B86-1 +44323932334238362d31 +# 31087270-D +33313038373237302d44 +# 18AB439E-F +31384142343339452d46 +# 083863F1-7 +30383338363346312d37 +# 33D9A762-9 +33334439413736322d39 +# 33D9A760-9 +33334439413736302d39 +# 4EFE2452-1 +34454645323435322d31 +# 33D9A761-9 +33334439413736312d39 +# E0F158E1-C +45304631353845312d43 +# 860BB310-5 +38363042423331302d35 +# 03D9F3F2-B +30334439463346322d42 +# _Marshaled_pUnk +5f4d61727368616c65645f70556e6b +# QuickTime.QuickTime +517569636b54696d652e517569636b54696d65 +# 02BF25D5-8C17 +30324246323544352d38433137 +# click to c +636c69636b20746f2063 +# PICT +50494354 +# +3c626f64793e3c7363726970743e61203d206e657720616374697665786f626a65637428276e6d73612e6d656469616465736372697074696f6e27293b612e6469737076616c7565203d20313b3c2f7363726970743e3c2f626f64793e3c2f68746d6c3e +# \x0d\x0aInstallTrigger.install.call(document," +0d0a3c7363726970743e0d0a496e7374616c6c547269676765722e696e7374616c6c2e63616c6c28646f63756d656e742c22 +# color=expression( +636f6c6f723d65787072657373696f6e28 +# color%3Dexpression%28 +636f6c6f7225334465787072657373696f6e253238 +# .class\xca\xfe\xba\xbe\x00\x00\x003\x00\x9d\x0a\x00$\x00N\x0a\x00#\x00O\x07\x00P\x0a\x00\x03\x00Q\x0a\x00\x03\x00R\x07\x00S\x0a\x00#\x00T\x07\x00U\x0a\x00\x08\x00N\x08\x00V\x0a\x00\x08\x00W\x09\x00X\ +2e636c617373cafebabe00000033009d0a0024004e0a0023004f0700500a000300510a000300520700530a002300540700550a0008004e0800560a0008005709005800590a0008005a08005b0a0008005c0a0006005d0a0006005e0a005f0060070061 +# \x00 \x00\x03\x00\x04\x00\x00\x00\x01\x10\x10\x00\x05\x00\x06\x00\x00\x00\x02\x00\x01\x00\x07\x00\x08\x00\x01\x00\x09\x00\x00\x00j\x00\x05\x00\x06\x00\x00\x00\x10*+\xb5\x00\x01*\x1c\x1d\x15\x04\x19\x +0020000300040000000110100005000600000002000100070008000100090000006a00050006000000102a2bb500012a1c1d15041905b70002b100000002000a0000000e00030000001e0005001f000f0020000b00000034000500000010000c000f00 +# PK\x03\x04\x0a\x00\x00\x08\x00\x00\x03r]C\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x09\x00\x04\x00META-INF/\xfe\xca\x00\x00PK\x03\x04\x0a\x00\x00\x08\x00\x00\x02r]C!L\x9c\x8f\xb1\x00\x00\x00\x +504b03040a000008000003725d43000000000000000000000000090004004d4554412d494e462ffeca0000504b03040a000008000002725d43214c9c8fb1000000b1000000140000004d4554412d494e462f4d414e49464553542e4d464d616e696665 +# ComboList +436f6d626f4c697374 +# ComboList +436f6d626f4c697374 +# VSFlexGrid.VSFlexGridL +5653466c6578477269642e5653466c6578477269644c +# c0a63b86-4b21-11d3-bd95-d426ef2c7949 +63306136336238362d346232312d313164332d626439352d643432366566326337393439 +# \xf4cb+\xe7\x09\xfc\x19p\x17\xcao\x06\x86\xe8e\xaf\xfc\x01\xe6\xd2\x02\xaf\\x13\x1c\x03\xae\x0fZ\xf4G!\xff7\xba78\x85\x95\xf1X\xf0j\x92\xa4g\x83_\xa0y\x12\x85\xee\xa0e\x12\xcb/\x92\x1aB'P +f463622be709fc197017ca6f0686e865affc01e6d202af5c131c03ae0f5af44721ff37ba37388595f158f06a92a467835fa0791285eea06512cb2f921a422750 +# \xfdW\x8e=*\x15\xf0\x97\xa4\x09R)G\x86\xa8\xb88\x07B\xa7<*T\x1a%\xcdW\xbd\x0e\x03\xaeo\x06\xbc\xfd\xadg7u\x16\x0a0Su0\x18e"g\xe4\xed\xf6\xf8\x98\xe3\x14666\xda\x991-\xa7 +fd578e3d2a15f097a40952294786a8b8380742a73c2a541a25cd57bd0e03ae6f06bcfdad673775160a3053753018652267e4edf6f898e314363636da99312da7 +# \xd4\xa2PQ\x14\xd1\x16\xe8G\xa5%\xa2jQ\xa5\x16\x15!\x90(mU*\xd4\x0fR\x0b\x15\xf4\xdc7\xb3\xbb\x13\x7f\xa0i\xa9Z\xa9\x8c=g\xde\xbd\xef\xbe\xfb\xee\xbb\xf3\xde\xbd\xef\xcd\xee\xbc\xfe\x04\x05\x88(\x88\ +d4a2505114d116e847a525a26a51a516152190286d552ad40f520b15f4dc37b3bb137fa069a95aa98c3d67debdefbefbeebbf3debdefcdeebcfe0405882888fb +# \x90\xe8i!\x16\x03\x7f\xe1&\xea\xb16h\xbcwy\x1d\x93\xfc\x0d\\xc8\xe3<\xe7\xa5\x0a\xac?WP\xf6\x88\xf9c\xac\x81\x0838\xfb\x9f[\xd5\xdd\xa5\x87\x1d\x98e\xbfo\xf7\xdbm\xa9N\xdds\xce\xbd\xe7\x9c{\xee\xb9\xf7\xdc[\xd5=|\xcb\xdd\xe4 "'\xae\xb7\xdf&z\x9c\xcc\xcf.z\xe7\xcfi\\x055\x7fQ +328404c83e083338fb9f5bd5dda5871d9865bf6ff7db6da94edd73cebde79c7beeb9f7dc5bd53d7ccbdde4202227aeb7df267a9ccccf2e7ae7cf695c05357f51 +# \xb7!~l\xbf\x9fW\x1d\xfc\xfa\xf4f\xe4\x89\x09\xc0 +b7217e6cbf9f571dfcfaf466e48909c0 +# SilverApp1.dllPK +53696c766572417070312e646c6c504b +# value="SilverApp1.xap" +76616c75653d2253696c766572417070312e78617022 +# \x0e\xf5L\x95\xda\xc0\xd4\x00\x89PA\x8f\xa3\xfa\xfb\x7f\xf3\xd6\xe3\x1c/\xee\xc1\xadg\x16,[-\xc4'U\xcb(\x9b9\xd5\xaab\x93\xaa\x92W\xcdv\xb9`\xe5\x0cS\xd7\xb2\xed\xf2Q\xd5\xb44\xa3\xd8\x9f\x8a'\xf8\xd +0ef54c95dac0d4008950418fa3fafb7ff3d6e31c2feec1ad67162c5b2dc42755cb289b39d5aa6293aa9257cd76b960e50c53d7b2edf251d5b434a3d89f8a27f8d72e0f9675bb6caafd45b56c9b8ade2e +# \xc6\xad\x02\xdb\xa2\xdb.7\xe0\xc6\xb0\x09cJf\x10\xc0\xd8\xc15\xb1\x8f0\xb1\x09\xe5\x80y'`\xecrn\x19\x13\x1a^\xf3\xa3\xf25\x12}\x91\xc4\x84\x95\xe8\xc8Qb\xaa\xc4}\xdb%\xe67t{\xce@\xaao\xfd\x05\x09\xe +c6ad02dba2db2e37e0c6b009634a6610c0d8c135b18f30b109e580792760ec726e19131a5ef3a3f235127d91c48495e8c85162aac47ddb25e637747bce40aa6ffd0509e682bcc9b3e12c133ce6672071a3b593edd825bcb6df730b2f98588718 +# PK\x03\x04\x14\x00\x00\x08\x08\x006\xaa\C\xca0>G\xc2\x00\x00\x00A\x01\x00\x00\x10\x00\x00\x00AppManifest.xaml\x85\x8fA\x0a\xc20\x10E\xf7\x82w\x089@\xd2\x0aV)V\x10t+E\xc5}MS\x1a\xc8$!\x89\x98\x9e\xcd\ +504b030414000008080036aa5c43ca303e47c200000041010000100000004170704d616e69666573742e78616d6c858f410ac2301045f78277083940d20a56295610742b45c57d4d531ac8242189989ecd8547f20ad6b64a1782db37f3fe9f79de1fab2d +# s\xd9\x84z\xf3\x9f\x84\xd0\xa0\xd9\x84P'{T\x86\xb0\xa1\x0b\xd0\x07.\xfa@\xce\x0f\xcb\xb4O\xad\xb1\x0d^prC\xc4/mR=\x8f^v\x9e\xbc\xb7\x0fU\x17\xd0\x81\xce\xda{\xfbQ\xbc\xe4\x91"\x0b\\xf2\xa1(J\xaa\xd4\ +73d9847af39f84d0a0d98450277b5486b0a10bd0072efa40ce0fcbb44fadb10d5e707243c42f6d523d8f5e769ebcb70f5517d081ceda7bfb51bce491220b5cf2a1284aaad4f32ee96d9768d44e6aa12e4e94f3ce42e5150babbee811845beb91 +# \x11\xd3\xc3\xa5\xa5\xbe\xebl\x1dc\xfb\x89\x0e+\x0e\xba\xf2\xea\xfd\xc7rz_!\xb5\xd9\xaf\xf8\x89v\x03\xd1,\xda\xd5\xdb\x00\x82\xb8N\xdb\xdeqY\xb5\xfc&*\xdc\xe9\xd3\x16\x9d?\x0e\xda\xf7~\xa2\xf5\xf2_\x +11d3c3a5a5beeb6c1d63fb890e2b0ebaf2eafdc7727a5f21b5d9aff8897603d12cdad5db0082b84edbde7159b5fc262adce9d3169d3f0edaf77ea2f5f25fb8e76ff29983de71b2f49e76acddc812dc5e +# PK\x03\x04\x14\x00\x00\x08\x08\x00U\xbfvC\xc0FG\x9c\xc8\x00\x00\x00]\x01\x00\x00\x10\x00\x00\x00AppManifest.xaml\x85\x8fA\x0a\xc20\x10E\xf7\x82w\x089@\xd2\x0aV)V\x10t+E\xc5}\x8dS\x0cd\x92\x90DM\xcf\x +^504b030414000008080055bf7643c046479cc80000005d010000100000004170704d616e69666573742e78616d6c858f410ac2301045f78277083940d20a56295610742b45c57d8d530c649290444dcfe6c2237905aba25410dccce2fdf9ffcfdc +# .applyElement +2e6170706c79456c656d656e74 +# .swapNode +2e737761704e6f6465 +# .appendChild(id +2e617070656e644368696c64286964 +# = document.createelement( +3d20646f63756d656e742e637265617465656c656d656e7428 +# \x00\x01\x00\x00standard jet db\x00 +^000100007374616e64617264206a657420646200 +# 7\x003\x00c\x009\x00d\x00f\x00a\x000\x00-\x007\x005\x000\x00d\x00-\x001\x001\x00e\x001\x00-\x00b\x000\x00c\x004\x00-\x000\x008\x000\x000\x002\x000\x000\x00c\x009\x00a\x006\x006 +370033006300390064006600610030002d0037003500300064002d0031003100650031002d0062003000630034002d003000380030003000320030003000630039006100360036 +# <\x08v\xa3\x80\xf90\xeb\xb49] t"\x8dB\xff\x89E\xb0\x0f\xbe\xc1\x83\xe8+t\xbcHH\x0f\x85q\xfe\xff\xff\x83M\x94\xffj\x07X\xe9z\xfd\xff\xffj\x0aXJ\x83\xf8\x0a\x0f\x85m\xfd\xff\xff\xebH3 +3c0876a380f930ebb4395d2074228d42ff8945b00fbec183e82b74bc48480f8571feffff834d94ff6a0758e97afdffff6a0a584a83f80a0f856dfdffffeb4833 +# \x0d\x0aEnd Sub\x0d\x0a\x0d\x0a\xd1\xdd\xb3\x92\xba\xdb\xaf\x9f\xe7\xbe\xafK +^4357530d80cd0000789c249b478ef4e0da96bf73fe5f88192031610d96704e43e79cb327c839967339ad80213b6205ccd8cfa13ed1ddb392badbaf9fe7beaf4b +# \xf6\xa6\x9d\xe9N\x7f8\x8a\xb6\xc4\xaf\x91\x8aoI\x12\xc9\xa9\x09+\x1d}\x07\xe4$\xd1\x8fGT\xf7LT~\xf2\x88\x07\xb8\xac\x0c\xb2\x87\x08V\xbcl\xady\x99\x82PV#\x9e\x07\xff\x88\x86bu\x9b;3\x99\xd9 +f6a69de94e7f388ab6c4af918a6f4912c9a9092b1d7d07e424d18f4754f74c547ef28807b8ac0cb2870856bc6cad7999825056239e07ff888662759b3b3399d9 +# CWS\x0dm\xed\x00\x00x\x9c$\x9bG\x0e\xf3\xda\x96\x9d\xff[\xf5\xf0\xbaU\x06j\x16\x04\x8a95\x99s\x16)\x92=\xe6\x9c3G\xe1\xe1\xb8\xe7\xae'\xe31<\xeb\xc2\x02\x045\x14x\xce\xe6\xdek}K\x94 +^4357530d6ded0000789c249b470ef3da969dff5bf5f0ba55066a16048a393599731629923de69c3347e1e1b8e7ae27e3313cebc20204351478cee6de6b7d4b94 +# \xaf\xfc\xdf\xff\xcf?\xb1\x02\xf0\x8e\xd1b +2765909035e289a7516b129cffa6af1b0155e616c5e9db4d4af6f494aba3e798e963274098399006a2e365c4710ec6291ffb3b3b6b4c0a3af2b72929953ed162 +# CWS\x0d\xa9\xdd\x00\x00x\x9c$\x9bG\xae\xf4l\xbb\x95\xdf\x8fsD\x97\x83\xc4,,\xe1\x1c\xaa\xe9\x9csv\x079g\xbb\x9c\xc3(\x18\x06\xb3`$\xcc\x83\xdeO\xbdbk7\xf7\xb6\xfd\xdca\xadk\x95U\xd2 +^4357530da9dd0000789c249b47aef46cbb95df8f73449783c42c2ce11caae99c7376073967bb9cc3281806b36024cc83de4fbd626b37f7b6fddc61ad6b9555d2 +# \xfd\xcf\x7f\x0e\xe80m}\x80l\x80\xd9\xbc\xf3y\xd0G\xd2\xebB\xc6\xdd\x8a\xd1O\x8d\x92\xcd\x17Nm\x19\xa49\xd0j\x95\xeb\x8c\xe7\x92\xfe\x0c\xc8\xc9~\x9d\x96\x0dL>\xe7Nd\x86\x84~m6\x02.\xf3d. +fdcf7f0ee8306d7d806c80d9bcf379d047d2eb42c6dd8ad14f8d92cd174e6d19a439d06a95eb8ce792fe0cc8c97e9d960d4c3ee74e6486847e6d36022ef3642e +# CWS\x0d\xc4\xca\x00\x00x\x9c$\x9a\xc7\x8e\xf3j\xd6^\xbf\xd3\xdd\xf0\xd4\xfe\x01\xdf\x05\x013\x93\xe2\x909g\x8a\x090\x0c\xe6\x9c3\xa7\x9ex\xe8;\xf2\xdc7\xd5\xd6\x81\xabP\xb3\x02D\xf1}\xf6\xb3\xd7\x92( +^4357530dc4ca0000789c249ac78ef36ad65ebfd3ddf0d4fe01df05013393e29039678a09300ce69c33a79e78e83bf2dc37d5d681ab50b30244f17df6b3d79228 +# \x00d\x00e\x00f\x00g\x00h\x00i\x00j\x00k\x00l\x00m\x00n\x00o\x00p\x00q\x00r\x00s\x00t\x00u\x00v\x00w\x00x\x00y\x00z\x00[\x00\\x00]\x00^\x00_\x00`\x00a\x00b\x00c\x00d\x00e\x00f\x00g\x00h\x00i\x00j\x00 +006400650066006700680069006a006b006c006d006e006f0070007100720073007400750076007700780079007a005b005c005d005e005f0060006100620063006400650066006700680069006a006b006c006d006e006f0070007100720073007400 +# cvt \x00d\x05\x11\x00\x00\x07\xac\x00\x00\x00\x04gasp\xff\xff\x00\x03\x00\x00v\x8c\x00\x00\x00\x08glyf\x05%ef\x00\x00\x09\xb4\x00\x00\x10\xf4head\xf56md\x00\x00\x00\xdc\x00\x00\x006hhea\x09\xf4\x04\x +6376742000640511000007ac0000000467617370ffff00030000768c00000008676c796605256566000009b4000010f468656164f5366d64000000dc000000366868656109f404050000011400000024686d74781ce60d5e000001b00000020a6c6f63 +# rw\x00\x00\xb0v\x00\x00\x01\x00\x02\x00\x00\x00\x00\x00\x02\x00\x06\x09\x00\x00\x00\x00\x00\x00\x01\x00\xf4\x01\x00\x00\x00\x00lp\x07\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x01\x +72770000b07600000100020000000000020006090000000000000100f401000000006c70070000000200000000000000000000000100000000000000ff3ad2700000000000000000000000000000000000000c0078006500630072006500740000000c +# FTP /pub at 172.16.8.207. +3c5449544c453e465450202f707562206174203137322e31362e382e3230372e3c2f5449544c453e +# stlt +73746c74 +# stlt +73746c74 +# RIFF +^52494646 +# document.body.contentEditable +646f63756d656e742e626f64792e636f6e74656e744564697461626c65 +# curv\x00\x00\x00\x00 +6375727600000000 +# 8589-11d1-B16A +383538392d313164312d42313641 +# \xd7\xcd\xc6\x9a +^d7cdc69a +# \x0areference +0a7265666572656e6365 +# .extractContents(); +2e65787472616374436f6e74656e747328293b +# document.createRange(); +646f63756d656e742e63726561746552616e676528293b +# DOMSubtreeModified +444f4d537562747265654d6f646966696564 +# DOMNodeRemoved +444f4d4e6f646552656d6f766564 +# "repro.xml", "i +22726570726f2e786d6c222c202269 +# .applyElement +2e6170706c79456c656d656e74 +# avcC\x01B\xc0\x0d\xff\xe1\x00\x1bgB\xc0\x0d\x9at\x0a\x0f\xdf\xf8\x07\x80\x0c\x98\x80\x00\x00\x03\x00\x80 +617663430142c00dffe1001b6742c00d9a740a0fdff807800c98800000030080 +# ftyp +66747970 +# sourcecode\IE_ParentProcess_SandboxEscape\x64\Release\TestDll.pd +736f75726365636f64655c49455f506172656e7450726f636573735f53616e64626f784573636170655c7836345c52656c656173655c54657374446c6c2e7064 +# estructor'\x00\x00\x00\x00\x00\x00`vector deleting destructor'\x00\x00\x00\x00`default constru +657374727563746f722700000000000060766563746f722064656c6574696e672064657374727563746f7227000000006064656661756c7420636f6e73747275 +# eId\x00\x00\x00\x00\x00GetTickCount64\x00\x00GetFileInformationByHandleExW\x00\x00\x00SetFileI +65496400000000004765745469636b436f756e743634000047657446696c65496e666f726d6174696f6e427948616e646c6545785700000053657446696c6549 +# BBFlashBack.FBRecorder +4242466c6173684261636b2e46425265636f72646572 +# a3cd4bf9-ec17-47a4 +61336364346266392d656331372d34376134 +# 436F626A64\d\\0\d +343336463632364136345c645c5c305c64 +# 021433412\d\\ +3032313433333431325c645c5c +# \\0D1BD8B85D111B16A\d\ +5c5c30443142443842383544313131423136415c645c +# =i\xe9Ho\xb6]0twn\x8e1jB@\x07\x95\xb6\xa5\x12\x8b\x94zY\x9a\x140@\xfc\xc5,\x8dZ%3o\x19\xb2\x09\x8bS\x02&g%s\x18 +3d69e9486fb65d3074776e8e316a42400795b6a5128b947a599a143040fcc52c8d5a25336f19b2098b53022667257318 +# ocStream.java\xad\x97]o\xdb \x14\x86\xaf\xe3_\x81z\xe5L\x99\x9b\x8b\xddE\x9b\xb6\xb6 +6f6353747265616d2e6a617661ad975d6fdb201486afe35f817ae54c999b8bdd459bb6b6 +# poc.java\xadTMO\x1b1\x10=\xef\xfe\x8aQN\xde(Z\x82JC\x11\xca\xa1\x05ZE\xa2\x05\x11\xe8\x15\x19\xef$1\xdd\xd8 +706f632e6a617661ad544d4f1b31103deffe8a514ede285a824a4311caa1055a45a20511e81519ef2431ddd8 +# \x00\x00.\x00\x00\x00 +00002e000000 +# AGNI +^41474e49 +# KEY* +4b45592a +# InsertUnorderedList +496e73657274556e6f7264657265644c697374 +# InsertOrderedList +496e736572744f7264657265644c697374 +# InsertOrderedList +496e736572744f7264657265644c697374 +# \x00\x00\x00\x00\x03\x00\x00\x00\x80\x03\x00\x00\x00\x00\x00\x00\x05\x00s\x00u\x00m\x00m\x00a\x00r\x00y\x00i\x00n\x00f\x00o\x00r\x00m\x00a\x00t\x00i\x00o\x00n\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\ +000000000300000080030000000000000500730075006d006d0061007200790069006e0066006f0072006d006100740069006f006e00000000000000000000000000000000000000000000000000000028000201ffffffffffffffffffffffff000000 +# \x00\x00\x00\xef\x00\x00\x00\xf0\x00\x00\x00\xf1\x00\x00\x00\xf2\x00\x00\x00\xf3\x00\x00\x00\xf4\x00\x00\x00\xf5\x00\x00\x00\xf6\x00\x00\x00\xf7\x00\x00\x00\xf8\x00\x00\x00\xf9\x00\x00\x00\xfa\x00\x0 +000000ef000000f0000000f1000000f2000000f3000000f4000000f5000000f6000000f7000000f8000000f9000000fa000000fb000000fc000000fd000000fe000000ff00000000010000010100000201000003010000040100000501000006010000 +# \xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00>\x00\x03\x00\xfe\xff\x09\x00\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x03\x00\x00\x00\x03\x00\x +d0cf11e0a1b11ae1000000000000000000000000000000003e000300feff0900060000000000000000000000030000000300000000000000001000000500000001000000feffffff00000000000000000100000002000000ffffffffffffffffffffff +# format(attr(r)); +666f726d61742861747472287229293b +# format(calc(poc));} +666f726d61742863616c6328706f6329293b7d +# ,""].join("");}< +2c22225d2e6a6f696e282222293b7d3c2f7363726970743e3c +# = [];for(var i = 0; i < 0x1800; i++) +203d205b5d3b666f72287661722069203d20303b2069203c203078313830303b20692b2b2920 +# = [];for(var i = 0; i < 0x1800; i++) { +203d205b5d3b666f72287661722069203d20303b2069203c203078313830303b20692b2b29207b +# \xf4E\x11\x00\xefz\x86WVe\x1c\x94n\xf5W\xa0\x94 4\x8c\x96\x0e1M\xdeC\xdf\xe7X8"x\x81G\x8aN\xe8\x9f\xfb\x1e}\xb0\xd3\xe6\xc2\x01\x8f\xba'\\x0e\x8e]\xe7\xbaJ\xc8\xad\xf5Z\xc6\x0aC\xf3 +f4451100ef7a865756651c946ef557a09420348c960e314dde43dfe75838227881478a4ee89ffb1e7db0d3e6c2018fba275c0e8e5de7ba4ac8adf55ac60a43f3 +# H\xbc\x9e\x88\xde0fX-\xc7\x05\x89\xdbI\xb8g?n4\xce\x00d\xe26\xde\xeaJ$\x9d9\x7f|\xbeY\x88\xfa\x16\xd8$\xf2\xc5eS+\xa6N}\x1d\xe0{\x04=\xf0\x9af\x99\xa0L\x9f\xdb\x08\x9dtU +48bc9e88de3066582dc70589db49b8673f6e34ce0064e236deea4a249d397f7cbe5988fa16d824f2c565532ba64e7d1de07b043df09a6699a04c9fdb089d7455 +# \x82\xdc\xdd\xfe\x9d\xfd$Y@f\x04S\xd6@Av\x10\xc8\xc5\xf2\xe4\xdb\xe2v\xe7 d1\xda\x84\x82l\x10\xdd/J\x03\xdf\x80f!\xb7\x0eL\)\xad\xd7\x0c\xe3\xad_S\xc7\xf8\x03[\x03=\x9b\xcf\xcf)\xb7 +82dcddfe9dfd245940660453d640417610c8c5f2e4dbe276e7206431da84826c10dd2f4a03df806621b70e4c5c29add70ce3ad5f53c7f8035b033d9bcfcf29b7 +# (sS,sSS);\x09\x09\x09\x09\x0d\x0afor (i=0;i\x09\x09\x09\x09\x09\x09\x0d\x0a\x0c\x00?\x00@\x01\x00!java/awt/image/ImagingOpException\x0c\x00A\x00\x16\x01\x00\x03 +726e656c0c0015003d0c0015003e0c003f00400100216a6176612f6177742f696d6167652f496d6167696e674f70457863657074696f6e0c00410016010003 +# \x00\x1fjava/awt/image/DataBufferUShort\x0c\x00\x15\x008\x0c\x006\x009\x07\x00:\x0c\x00;\x00<\x01\x00\x19java/awt/image/ConvolveOp\x01\x00\x15java/awt/image/Ke +001f6a6176612f6177742f696d6167652f446174614275666665725553686f72740c001500380c0036003907003a0c003b003c0100196a6176612f6177742f696d6167652f436f6e766f6c76654f700100156a6176612f6177742f696d6167652f4b65 +# \x16\x01\x00\x1djava/awt/image/DataBufferByte\x0c\x00\x15\x003\x01\x00\x0ejava/awt/Point\x0c\x00\x15\x004\x07\x005\x0c\x006\x007\x01 +1601001d6a6176612f6177742f696d6167652f44617461427566666572427974650c0015003301000e6a6176612f6177742f506f696e740c001500340700350c0036003701 +# , 21000001);\x0d\x0adocument.write( +2c203231303030303031293b0d0a646f63756d656e742e777269746528 +# ;\x0d\x0a} while (0 < -- +3b0d0a7d207768696c65202830203c202d2d +# = unescape("%u4141%u4141");\x0d\x0avar +203d20756e657363617065282225753431343125753431343122293b0d0a76617220 +# \x00\x00\x01\xba +^000001ba +# f.removeChild(document.getElementsByTagName("img")[0]) +662e72656d6f76654368696c6428646f63756d656e742e676574456c656d656e747342795461674e616d652822696d6722295b305d29 +# var f = document.getElementsByTagName("form")[0] +7661722066203d20646f63756d656e742e676574456c656d656e747342795461674e616d652822666f726d22295b305d +#
\x0a\x0a
+3c666f726d3e0a3c696d672069643d2269223e0a3c2f666f726d3e +# innerHTML = "PASS" +696e6e657248544d4c203d20225041535322 +# innerHTML = "FAIL" +696e6e657248544d4c203d20224641494c22 +# f.removeChild(document.getElementsByTagName("img")[0]) +662e72656d6f76654368696c6428646f63756d656e742e676574456c656d656e747342795461674e616d652822696d6722295b305d29 +# var f = document.getElementsByTagName("form")[0] +7661722066203d20646f63756d656e742e676574456c656d656e747342795461674e616d652822666f726d22295b305d +#
\x0a\x0a
+3c666f726d20636c6173734e616d653d2261223e0a3c696d67207372633d22222069643d2269223e0a3c2f666f726d3e +# imgBar +696d67426172 +# imgBar +696d67426172 +# imgFoo +696d67466f6f +# imgBar +696d67426172 +# imgFoo +696d67466f6f +# CURLOPT_POSTFIELDS +4355524c4f50545f504f53544649454c4453 +# openssl_seal +6f70656e73736c5f7365616c +# %3e%3c/content%3e%0a%3c/binding%3e%3c/bindings%3e);" +2533652533632f636f6e74656e742533652530612533632f62696e64696e672533652533632f62696e64696e6773253365293b22 +# binding%20id%3d%22a%22%3e%0a%3ccontent%3e%3cchildren/%3e%3c/con +62696e64696e67253230696425336425323261253232253365253061253363636f6e74656e742533652533636368696c6472656e2f2533652533632f636f6e +# xmlns%3d%22http%3a//www.mozilla.org/xbl%22%3e%0a%3cbinding%20id +786d6c6e73253364253232687474702533612f2f7777772e6d6f7a696c6c612e6f72672f78626c25323225336525306125336362696e64696e672532306964 +# g:url(data:text/xml;charset=utf-8,%3cbindings%20xmlns%3d%22http +673a75726c28646174613a746578742f786d6c3b636861727365743d7574662d382c25336362696e64696e6773253230786d6c6e7325336425323268747470 +# moz-binding:url +6d6f7a2d62696e64696e673a75726c +# binding.xml#a +62696e64696e672e786d6c2361 +# https://bugzilla.mozilla.org/attachment.cgi?id=277817#a +68747470733a2f2f6275677a696c6c612e6d6f7a696c6c612e6f72672f6174746163686d656e742e6367693f69643d3237373831372361 +# moz-binding:url +6d6f7a2d62696e64696e673a75726c +# "].random = null;[" +225d2e72616e646f6d203d206e756c6c3b5b22 +# "].random;}\x16\xf3\xf5\x12\x92\xaf\x1e\xaf>estkc\x10\xf8\xde3\x9e\xfb\xe9\xd1\xabhrc\xeb\xf5\x18|\x8b\xe8\x981~vh\x1a\xa9pq\xfab~\xbb\x80m\x1a\x0c\xd8h\xb5\xcdd\xd86i\x94`\x93\x8f\xbei\x03\xb6)\x +852a06823e16f3f51292af1eaf3e6573746b6310f8de339efbe9d1ab687263ebf5187c8be898317e76681aa97071fa627ebb806d1a0cd868b5cd64d836699460938fbe6903b629e1cbb57c29e472c8e5845c66d66e303bc1ec26985dc694a1830729b717 +# 5\xb6-\xb9\xcbvw\x06\xc8\x18}\xf4\xda\x03\xf55\xfa\x0ehh[c,'\x86\xc6\xa1\x0a\x9a\xdbi\xa36)\xad\xa1&\xbc[n\xb7]\x80^\x0f\xd1\xc3\x91\\xe8l\xf48\xd4\xd2\x06\xa7\xafx\x9cl$\xea\xd5\x10f\xb2\xde\xfd\x13 +35b62db9cb767706c8187df4da03f535fa0e68685b632c2786c6a10a9adb69a33629ada126bc5b6eb75d805e0fd1c3915ce86cf438d4d206a7af789c6c24ead51066b2defd13676a9fe8c1616d3dba776834f705dfbd193bdc97f028723dd79a651ccff0 +# %\xf1~<\xe3\x8a_\xc9\xb8\xed\x8c\xb9+\xeb6\xf3[;\xbf63`\xbbr\xbb\xac\xbb\xad3\xc0\xbd\xe1i-\x1erlv\x07too\xd0\xcc\xd1\x87fe^e\xd9_\xcb\xcc\xef<\xf7\x1b\x06\xe1!\xce\xa3\xef|\xc7y\xdbw\x90\xd0\x0f\xf0 +25f17e3ce38a5fc9b8ed8cb92beb36f35b3bbf363360bb72bbacbbad33c0bde1692d1e726c7607746f6fd0ccd18766655e65d95fcbccef3cf71b06e121cea3ef7cc779db7790d00ff09d8226e1ff9c2e327ce5d7b6c39ecb787584751499ae2bf4f766d2 +# p\x1c\xcb\x02\x8ecy@\x92,\x0b`y\x00m\x03x\x1a@\x14\x01\x80\x00\x00\x80\x02\x07\x00\x80\x00\x1b4%\x16\x07(4d%\x00\x10\x05\x00\xe0p\x14\xcb\xd24q\xe48\x96\xa5i\xa2\xc8q,k\xd3d\x91ei\x9a\xa6\x89"4k\xd3d +701ccb028e637940922c0b6079006d03781a4014018000008002070080001b342516072834642500100500e07014cbd23471e43896a569a2c8712c6bd3649165699aa68922346bd364119ee779a609cff33cd384288aa2690271346d010000050e0000 +# \xae\xeb\xba\xae\xeb\xba\xae\xeb\xba\xae\xeb\xba\xae\xeb\xba\xae\xeb\xba\xae\xeb\xba\xae\xeb\xba\xae\xeb\xba\xae\xeb\xba\xae\xeb\xba\xae\xeb\xba@h\xc8*\x00@\x02\x00@gr$gr$er$er$\x07\x08\x0dy\x05\x00\ +aeebbaaeebbaaeebbaaeebbaaeebbaaeebbaaeebbaaeebbaaeebbaaeebbaaeebbaaeebba4068c82a004002004067722467722465722465722407080d790500c800000800c0311c637224c7b22c6df3346ff334d1133dd1333d75746517080d79050000 +# =%n\x01\x05vorbis)bcv\x01\x00\x08\x00\x00\x001l \xc5\x80\xd0\x90u\x00\x00\x10\x00\x00`$)\x0e\x93fi)\xa5\x94\xa1(y\x98\x94hi)\xa5\x94\xc50\x89\x98\x94\x89\xc5\x18c\x8c1\xc6\x18c\x8c1\xc6\x18c\x8c 4d\x +3d256e0105766f7262697329626376010008000000316c20c580d0907500001000006024290e93666929a594a128799894686929a594c53089989489c518638c31c618638c31c618638c20346415000004008028098ea3e6696ace396718278e72a039 +# parseFloat("NAN(ffffeaaaaaaaa)") +7061727365466c6f617428224e414e2866666666656161616161616161292229 +# parseFloat("NAN(ffffeeeeeff0f)") +7061727365466c6f617428224e414e2866666666656565656566663066292229 +# A0X1V0O0F0F0E0R0S1V0D0I0S0P0L0A0YzutB2R0M0A0X1V0O0F0F0E0R0S1V0T0A0K0E0NzutB2R0D0O0W0N0L0O +4130583156304f3046304630453052305331563044304930533050304c304130597a7574423252304d304130583156304f30463046304530523053315630543041304b3045304e7a75744232523044304f3057304e304c304f3c2f2324404024233e +# 1M2Z2Z1Ezx0/tE1Q0o0w0ntFtA06000s1T0f0etF0c0o1HtE0i1G0s0t0.1P2V0ezs0e0x1P0?0F0F0F0F0F0F0F0E0E0E0E0E0E +314d325a325a31457a78302f74453151306f3077306e7446744130363030307331543066306574463063306f314874453069314730733074302e3150325630657a73306530783150303f3046304630463046304630463046304530453045304530453045 +# \xc7:\x06\xfa\xbd6\xd5^\xba-\xc0E\xd9\x0b{\xa0d\x16\xbf\xf4\xee\xec\xf3v\xf8\x83\xc6\xd4r=s\xcf~\xaa^Pk\xd5\xe1+\xa9\xf7\x00\x00\x00\x00\x00\x00<#$@@$#>\x120P0R0O0D0U0C0T1V0T0I0T0L0Ezu6M5Z4U6K3Y4 +c73a06fabd36d55eba2dc045d90b7ba06416bff4eeecf376f883c6d4723d73cf7eaa5e506bd5e12ba9f70000000000003c2324404024233e1230503052304f30443055304330543156305430493054304c30457a75364d355a3455364b335934 +# T0A0K0E0N0=020|0D0O0W0N0L0O0A0D0E0R0_0V0E0R0S0I0O0N0=01 +543041304b3045304e303d3032307c3044304f3057304e304c304f3041304430453052305f30563045305230533049304f304e303d30313c2f2324404024233e +# <#$@@$#>\x120P0R0O0D0U0C0T0_0T0I0T0L0E0=0C0V0E0-020001030-030900000 +3c2324404024233e1230503052304f3044305530433054305f305430493054304c3045303d304330563045302d3032303030313033302d303330393030303030 +# \x880\x00\x00\x00\x02\x02\x000\x82\x18-\x06\x09*\x86H\x86\xf7\x0d\x01\x07\x02\xa0\x82\x18\x1e0\x82\x18\x1a\x02\x01\x011\x0b0\x09\x06\x05+\x0e\x03\x02\x1a\x05\x000h\x06\x0a+\x06\x01\x04\x01\x827\x02\x +88300000000202003082182d06092a864886f70d010702a082181e3082181a020101310b300906052b0e03021a05003068060a2b060104018237020104a05a30 +# 00(wrhc&)00(wrhc&)00(wrhc&)00(wrhc&)10(wrhc&)6712(wrhc&)10 +30302877726863262930302877726863262930302877726863262930302877726863262931302877726863262936373132287772686326293130 +# edocllehsnur +65646f636c6c6568736e7572 +# hit ctrl-o, cancel, click me\x0a\xbc\x0e\xa6\xa2j\xf8 /d\xd8\xf2O(Z#\x97\xb4\xb5}\xd5\xdd\xdb\xd8\x97\xa9*\7/\xb1l\x8b\xb0e\xe4\xd2\x0fq9M\ +826748852adc7e42d2e889f3b80006fde135f976bc056fc8c4bbe510505c2f3ebc0ea6a26af8202f64d8f24f285a2397b4b57dd5dddbd897a92a5c372fb16c8bb065e4d20f71394db69ac8a71f199878b25762ab23689bf8f30f +# \xf1\xde\x1a\xed+\xb1\x0a\x12\x11C\xbc\x88f=\x9b<`j\xa2\xa4F+\x01\xec}\xe8\xcc;0d7\xd8"\xf0\xaflk\x88\x1f\xbdQ(\xbc\x884g\x07$km\x98B\x02\xac\xdc\x8f\xe2\xf8\xc8\xeeZ\xf0\x12\x8b@c\x0dImZ+\xc1\x83a\x +f1de1aed2bb10a121143bc88663d9b3c606aa2a4462b01ec7de8cc3b306437d822f0af6c6b881fbd5128bc88346707246b6d984202acdc8fe2f8c8ee5af0128b40630d496d5a2bc18361a2c5a761859aa7d7059a0e7320a29521 +# \x9a%\x83+X)\xaa\xa4\xd4x"~c\x17\x9e\x8c\xe2\xbc:\xd9[\x09pR\x1e\x9f[\xaew\x9b\xddx\x9a%\xa3*\xd6\xcc=\xc6*\x87#\x9eXh\x9bE\xe9\x1a\xa5*Q;\xf6\xa7Z\xbaAa\x0c`\x94\xe3\x81\xfd\xe3\xaa\xf0\x18\xc0F\xaa +9a25832b5829aaa4d478227e63179e8ce2bc3ad95b0970521e9f5bae779bdd789a25a32ad6cc3dc62a87239e58689b45e91aa52a513bf6a75aba41610c6094e381fde3aaf018c046aa5ea51a845d1b645d7b9a25832b5829aca4 +# \x97-\x86\xf8E\xd2\x80\xebI\x94Ik%S\x9c\x1eRs\x09[\xd9_\xec\x8c\xc6]Tsc\x1f\xf2\xae\xf1\xbfl\xaf\xfa\xea\xdf\xd5\xde\x0d\x91\xd5\xb8f\\xae\x1bU\xe4\x89\x97L%|\x81H\xc7\x1azR\x83KL\x1atL\xa2\x9a\x9a\x +972d86f845d280eb4994496b25539c1e5273095bd95fec8cc65d5473631ff2aef1bf6caffaeadfd5de0d91d5b8665cae1b55e489974c257c8148c71a7a52834b4c1a744ca29a9a8414039cc8a6d5f1ad70919049af3a9f38b514 +# |\xe9W\x9fl\xd4\xad\xc7\xdb\xcb\xe0\xa8\x92&\xdfW=\xef0\xbc\x8c\x8e\x1fl\xd5\x04\x1d\xc4\x01\xdc8Aj\xe4\xa6\x00VH\x0e\xa6{\xdb\x12\x03\x8c\xd3&Nn\x8e\^y\xec\xfd8\x91\x16\xed\xbb\xb0\xfb\x9d['`\x9fg\x +7ce9579f6cd4adc7dbcbe0a89226df573def30bc8c8e1f6cd5041dc401dc38416ae4a60056480ea67bdb12038cd3264e6e8e5c5e79ecfd389116edbbb0fb9d5b27609f67bf2b6e8a66e1b90aea16b37f0af0449c3c7850d3d389 +# yI0\x84\xb2\x11\x9bx\xc6\x15F\x09\xb5n\xeeg\x14\x04pG8\x89\xb9\x10fF\x13\x92\xd4\xb4\xa4(w\xe9Msz\xb3\x93\x9fT\xd5$\xe7\x99\xceuz2\x93\xeft\xe7;\xffQXHqz\x86\x9c\x9d\xec#?\xab\x19Jw\xc4`\x9f\x08u\xce +79493084b2119b78c6154609b56eee67140470473889b91066461392d4b4a42877e94d737ab3939f54d524e799ce757a3293ef74e73bff515848717a869c9dec233fab194a77c4609f0875ce1ff469a252e27121e14ca8449f73 +# \x849\xc1Q-\xb1\xcf\xcd\x16\x9c\xb7\xef\xb7\xe7\xba\x14\xa2A]\xd7\xa5\x97\xfd\xcb\x8d\xe1\xb0Y\x92s\x15\xe1B\x04\xcfs\xe1\x0e>+\xfc\xb7=\xfdE8\x00\x8b8\xbe\xae\x17\x85\xab~\x1b\x80\x1f\xff\x12\xc7KQ5 +8439c1512db1cfcd169cb7efb7e7ba14a2415dd7a597fdcb8de1b059927315e14204cf73e10e3e2bfcb73dfd4538008b38beae1785ab7e1b801fff12c74b5135bf285c992aca7cbaf4b6e691a9af5eae42f1eb4ebe62c1505bc6 +# \x00\x00\x1cdref\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00\x0curl \x00\x00\x00\x01\x00\x00\x03`stbl\x00\x00\x00\stsd\x00\x00\x00\x00\x00\x00\x00\x01\x00\x00\x00Lmp4a\x00\x00\x00\x00\x00\x00\x00\x01 +00001c6472656600000000000000010000000c75726c2000000001000003607374626c0000005c7374736400000000000000010000004c6d703461000000000000000100000000000000000002001000000000ac440000000000 +# \x0bg\x00\x00\x00:\x00\x00\x00\xac\x00\x00\x00\x9b\x00\x00\x009\x00\x00\x006\x00\x00\x00:\x00\x00\x006\x00\x00\x006\x00\x00\x006\x00\x00\x006\x00\x00\x006\x00\x00\x006\x00\x00\x006\x00\x00\x01O\x00\x +0b670000003a000000ac0000009b00000039000000360000003a000000360000003600000036000000360000003600000036000000360000014f0000139c00000394000003d7000003f1000004b80000044d00000631000004de +# \x02\xf8\x00\x00\x02\xf9\x00\x00\x00(stsc\x00\x00\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x1b\x00\x00\x00\x01\x00\x00\x00\x1d\x00\x00\x00\x05\x00\x00\x00\x01\x00\x00\x0b\xf8stsz\x00\x00\x +02f8000002f900000028737473630000000000000002000000010000001b000000010000001d000000050000000100000bf87374737a0000000000000000000002f90000025200000485000004bf0000036e0000034e0000045b +# t\xc3X\xe7\xc2\xe3\x10R?\xfecC\xe3\xc8K\xc0\xdfe{\x03\x13\xf1I\x80\x80\xe8\xdf\x10\x8f\x19\x19!\xc3_\xab\xac\xcc"\x8b\xf7\xbb~\x7f\xabO\xc9\x92P?\xfc\x17\xfc\x03\x11P]\xe7qzP\xa5\x0a\xebJ\xa9\x16\x99 +74c358e7c2e310523ffe6343e3c84bc0df657b0313f1498080e8df108f191921c35fabaccc228bf7bb7e7fab4fc992503ffc17fc0311505de7717a50a50aeb4aa916999e09a8b9e0fe533e8f4ba0b813a9f28edbcc93331c3d02 +# dia\x00\x00\x00 mdhd\x00\x00\x00\x00\xce\xc8sL\xce\xc8sL\x00\x00\x03\xe8\x00\x00\x00.U\xc4\x00\x00\x00\x00\x004hdlr\x00\x00\x00\x00\x00\x00\x00\x00soun\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00 +646961000000206d64686400000000cec8734ccec8734c000003e80000002e55c400000000003468646c720000000000000000736f756e0000000000000000000000004e65754c696f6e204d5034202d20417564696f00000001 +# \x05k\x00\x00\x06\x9b\x00\x00\x06\xad\x00\x00\x07\xc4\x00\x00\x08+\x00\x00\x09%\x00\x00\x0am\x00\x00\x07\x8c\x00\x00\x09\xe5\x00\x00\x09\x9b\x00\x00\x0b\x0e\x00\x00\x0b|\x00\x00\x08\x90\x00\x00\x0aD\ +056b0000069b000006ad000007c40000082b0000092500000a6d0000078c000009e50000099b00000b0e00000b7c0000089000000a4400000b1700000b0900000c9800000bf0000004ee000010670000009f00000e6c000000f1 +# \xa1\xa1\xa1\xa0\x00\x00\x00\xd0\xd0\xd0\xd0\x00\x00\x00hhhh\x00\x00\x004444\x00\x00\x00\x1a\x1a\x1a\x1a\x00\x00\x00}\x0d\x0d\x0d\x00\x00\x00\x06\x86\x86\x86\x80\x00\x00\x03\x03CCC@\x00\x00\x01\xa1\x +a1a1a1a0000000d0d0d0d000000068686868000000343434340000001a1a1a1a0000007d0d0d0d00000006868686800000030343434340000001a1a1a1a0000000d0d0d0d000000068686868000000343434340000001a1a1a1a +# \x06\xc3\x00\x00\x05G\x00\x00\x07\x80\x00\x00\x07\x04\x00\x00\x04S\x00\x00\x06\xc1\x00\x00\x06F\x00\x00\x06\xcb\x00\x00\x072\x00\x00\x07D\x00\x00\x08\x88\x00\x00\x06\xb2\x00\x00\x06M\x00\x00\x06g\x00 +06c300000547000007800000070400000453000006c100000646000006cb000007320000074400000888000006b20000064d00000667000006440000087c000006800000062a0000064900000622000007ff000005f900000696 +# \x86\x86\x80\x00\x00\x03\x03CCC@\x00\x00\x01\xa1\xa1\xa1\xa0\x00\x00\x00\xd0\xd0\xd0\xd0\x00\x00\x00hhhh\x00\x00\x004444\x00\x00\x00\x1a\x1a\x1a\x1a\x00\x00\x00\xbb\x0d\x0d\x0d\x00\x00\x00\x06\x86\x8 +8686800000030343434340000001a1a1a1a0000000d0d0d0d000000068686868000000343434340000001a1a1a1a000000bb0d0d0d00000006868686800000030343434340000001a1a1a1a0000000d0d0d0d000000068686868 +# \x04\xee\x01\x09\xcc\x1f +ce50a7ed067a424702d030da29d4e54d1416c1de9163d10e24b01ee1dc30c3022e305c2143ec276e470e623e0109cc1f +# 3G\xe00\x0a\x82(\x88\x14p\x1c\xc7(\xf8&\xb1GY\xca\xee\x80\x19\xc0\x14\xec\x01\x8c)\xe5V\xb0\x87\xcd8\x02\x1fK\x16\x00\x0e\x08\x07\x91DHF\x0f@ +3347e0300a82288814701cc728f826b14759caee8019c014ec018c29e556b087cd38021f4b16000e0807914448460f40 +# II*\x00\x84\x17\x00\x00\x80\x10`d\x18\x19\x04\xe2\x08\x04\x01\xc0\x00\xc0H\x01\xe8\xf7|\x00\x00 \x80\x08D\x10\x0d\x04:\x1f\xc0`\x80=\xf0\xf8|\xbf@\x0f\x87\xd0`2\x1a +^49492a008417000080106064181904e2080401c000c04801e8f77c000020800844100d043a1fc060803df0f87cbf400f87d060321a +# document.styleSheets[0].cssText = "p{text-overflow:ellipsis;overflow-x:hidden;} p:first-letter{float:left;}" +646f63756d656e742e7374796c655368656574735b305d2e63737354657874203d2022707b746578742d6f766572666c6f773a656c6c69707369733b6f766572666c6f772d783a68696464656e3b7d20703a66697273742d6c65747465727b666c6f61743a6c6566743b7d22 +# obj["onpropertychange"]=function(e){ obj2["applyElement"](all_elements_list[5]);} +6f626a5b226f6e70726f70657274796368616e6765225d3d66756e6374696f6e2865297b206f626a325b226170706c79456c656d656e74225d28616c6c5f656c656d656e74735f6c6973745b355d293b7d +# obj2["applyElement"](all_elements_list[5]) +6f626a325b226170706c79456c656d656e74225d28616c6c5f656c656d656e74735f6c6973745b355d29 +# var element_2=document.createElement("linearGradient") +76617220656c656d656e745f323d646f63756d656e742e637265617465456c656d656e7428226c696e6561724772616469656e742229 +# script id="a" onpropertychange="boom() +7363726970742069643d226122206f6e70726f70657274796368616e67653d22626f6f6d2829 +# Y\xc3\x90s\xdc)\xc5f\xa0\x09Y\x86 +59c39073dc29c566a0095986 +# \x9a\x89\xa6\xd8\x1a\x05ik\xae@\xb4\x87X +9a89a6d81a05696bae40b48758 +# \x80E`2\x9bK3I\xd9 +804560329b4b3349d9 +# \xb2\x9bV>\xb8_\x9fq +b29b563eb85f9f71 +# \x1f\x00\x01\xf0\x12\x05\x00\x00"\x00\x07\xf0\x0a +1f0001f012050000220007f00a +# \x00"$"#,##0_ +00222422232c2323305f +# [4\PZX54(P +5b345c505a5835342850 +# .txt\x7f\x02\x01\x80\xff\xff\xd0\xff +2e7478747f020180ffffd0ff +# \x14\x02\x00\xefZ\x083h\x0d\xed= +140200ef5a0833680ded3d +# \xde\xba\x90js\xff\xf3\x82d\xf3\x1a\x8a0\xf2\x001\xebn\x00\x00\x03H +deba906a73fff38264f31a8a30f20031eb6e00000348 +# u\xb1?\xff\xff\xff\xff\xff\xff\xff\xff\xff\xfdm\xff\xff\x80\xff\x04\xd5 +75b13ffffffffffffffffffffd6dffff80ff04d5 +# FWS +^465753 +# \xd4\xc3\xb2\xa1 +^d4c3b2a1 +# \xa1\xb2\xc3\xd4 +^a1b2c3d4 +# width=1000 height=100>\x0d\x0a