diff --git a/newrelic/config.py b/newrelic/config.py index 952a41b70..9166fbd3d 100644 --- a/newrelic/config.py +++ b/newrelic/config.py @@ -237,6 +237,9 @@ def _map_default_host_value(license_key): return license_key +def _map_comma_separated_values(s): + return list(map(str.strip, s.split(","))) + # Processing of a single setting from configuration file. @@ -346,6 +349,32 @@ def _process_configuration(section): _process_setting(section, "ca_bundle_path", "get", None) _process_setting(section, "audit_log_file", "get", None) _process_setting(section, "monitor_mode", "getboolean", None) + _process_setting(section, "security.agent.enabled", "getboolean", None) + _process_setting(section, "security.enabled", "getboolean", None) + _process_setting(section, "security.mode", "get", None) + _process_setting(section, "security.validator_service_url", "get", None) + _process_setting(section, "security.detection.rci.enabled", "getboolean", None) + _process_setting(section, "security.detection.rxss.enabled", "getboolean", None) + _process_setting(section, "security.detection.deserialization.enabled", "getboolean", None) + _process_setting(section, "security.exclude_from_iast_scan.api", "get", _map_comma_separated_values) + _process_setting(section, "security.exclude_from_iast_scan.http_request_parameters.header", "get", _map_comma_separated_values) + _process_setting(section, "security.exclude_from_iast_scan.http_request_parameters.query", "get", _map_comma_separated_values) + _process_setting(section, "security.exclude_from_iast_scan.http_request_parameters.body", "get", _map_comma_separated_values) + _process_setting(section, "security.exclude_from_iast_scan.iast_detection_category.insecure_settings", "getboolean", None) + _process_setting(section, "security.exclude_from_iast_scan.iast_detection_category.invalid_file_access", "getboolean", None) + _process_setting(section, "security.exclude_from_iast_scan.iast_detection_category.sql_injection", "getboolean", None) + _process_setting(section, "security.exclude_from_iast_scan.iast_detection_category.nosql_injection", "getboolean", None) + _process_setting(section, "security.exclude_from_iast_scan.iast_detection_category.ldap_injection", "getboolean", None) + _process_setting(section, "security.exclude_from_iast_scan.iast_detection_category.javascript_injection", "getboolean", None) + _process_setting(section, "security.exclude_from_iast_scan.iast_detection_category.command_injection", "getboolean", None) + _process_setting(section, "security.exclude_from_iast_scan.iast_detection_category.xpath_injection", "getboolean", None) + _process_setting(section, "security.exclude_from_iast_scan.iast_detection_category.ssrf", "getboolean", None) + _process_setting(section, "security.exclude_from_iast_scan.iast_detection_category.rxss", "getboolean", None) + _process_setting(section, "security.request.body_limit", "get", None) + _process_setting(section, "security.scan_schedule.schedule", "get", None) + _process_setting(section, "security.scan_schedule.duration", "getint", None) + _process_setting(section, "security.scan_schedule.delay", "getint", None) + _process_setting(section, "security.scan_schedule.always_sample_traces", "getboolean", None) _process_setting(section, "developer_mode", "getboolean", None) _process_setting(section, "high_security", "getboolean", None) _process_setting(section, "capture_params", "getboolean", None) @@ -4858,6 +4887,26 @@ def _setup_agent_control_health(): agent_control_health_thread.start() +def _setup_security_module(): + """Initiates security module and adds a + callback to agent startup to propagate NR config + """ + try: + if not _settings.security.agent.enabled or _settings.high_security: + _logger.warning("New Relic Security is disabled by one of the user provided config `security.agent.enabled` or `high_security`.") + return + from newrelic_security.api.agent import get_agent + + # initialize security agent + security_agent = get_agent() + # create a callback to reinitialise the security module + newrelic.core.agent.Agent.run_on_startup(security_agent.refresh_agent) + except ImportError: + _logger.warn("Security Agent isn't available") + except Exception as csec_error: + _logger.error("Security Agent Startup failed with error %s", csec_error) + + def initialize( config_file=None, environment=None, @@ -4877,7 +4926,9 @@ def initialize( ignore_errors = newrelic.core.config._environ_as_bool("NEW_RELIC_IGNORE_STARTUP_ERRORS", True) _load_configuration(config_file, environment, ignore_errors, log_file, log_level) - + + _setup_security_module() + _setup_agent_control_health() if _settings.monitor_mode: diff --git a/newrelic/core/config.py b/newrelic/core/config.py index 02096401c..efbcbf093 100644 --- a/newrelic/core/config.py +++ b/newrelic/core/config.py @@ -362,6 +362,44 @@ class ApplicationLoggingLocalDecoratingSettings(Settings): pass +class SecuritySettings(Settings): + pass + + +class SecurityDetectionSettings(Settings): + pass + + +class SecurityAgentSettings(Settings): + pass + + +class SecurityDetectionRCISettings(Settings): + pass + + +class SecurityDetectionRXSSSettings(Settings): + pass + + +class SecurityDetectionDeserializationSettings(Settings): + pass + +class SecurityRequestSettings(Settings): + pass + +class SecurityScanScheduleSettings(Settings): + pass + +class SecurityExcludeFromIASTScanSettings(Settings): + pass + +class SecurityExcludeFromIASTScanHTTPRequestParametersSettings(Settings): + pass + +class SecurityExcludeFromIASTScanIASTDetectionCategorySettings(Settings): + pass + class InfiniteTracingSettings(Settings): _trace_observer_host = None @@ -493,6 +531,19 @@ class EventHarvestConfigHarvestLimitSettings(Settings): _settings.message_tracer = MessageTracerSettings() _settings.process_host = ProcessHostSettings() _settings.rum = RumSettings() +_settings.security = SecuritySettings() +_settings.security.agent = SecurityAgentSettings() +_settings.security.detection = SecurityDetectionSettings() +_settings.security.detection.deserialization = SecurityDetectionDeserializationSettings() +_settings.security.detection.rci = SecurityDetectionRCISettings() +_settings.security.detection.rxss = SecurityDetectionRXSSSettings() +_settings.security.exclude_from_iast_scan = SecurityExcludeFromIASTScanSettings() +_settings.security.exclude_from_iast_scan.http_request_parameters = \ + SecurityExcludeFromIASTScanHTTPRequestParametersSettings() +_settings.security.exclude_from_iast_scan.iast_detection_category = \ + SecurityExcludeFromIASTScanIASTDetectionCategorySettings() +_settings.security.request = SecurityRequestSettings() +_settings.security.scan_schedule = SecurityScanScheduleSettings() _settings.serverless_mode = ServerlessModeSettings() _settings.slow_sql = SlowSqlSettings() _settings.span_events = SpanEventSettings() @@ -510,7 +561,6 @@ class EventHarvestConfigHarvestLimitSettings(Settings): _settings.transaction_tracer.attributes = TransactionTracerAttributesSettings() _settings.utilization = UtilizationSettings() - _settings.log_file = os.environ.get("NEW_RELIC_LOG", None) _settings.audit_log_file = os.environ.get("NEW_RELIC_AUDIT_LOG", None) @@ -1007,6 +1057,65 @@ def default_otlp_host(host): _settings.package_reporting.enabled = _environ_as_bool("NEW_RELIC_PACKAGE_REPORTING_ENABLED", default=True) _settings.ml_insights_events.enabled = _environ_as_bool("NEW_RELIC_ML_INSIGHTS_EVENTS_ENABLED", default=False) +_settings.security.agent.enabled = _environ_as_bool("NEW_RELIC_SECURITY_AGENT_ENABLED", False) +_settings.security.enabled = _environ_as_bool("NEW_RELIC_SECURITY_ENABLED", False) +_settings.security.mode = os.environ.get("NEW_RELIC_SECURITY_MODE", "IAST") +_settings.security.validator_service_url = os.environ.get("NEW_RELIC_SECURITY_VALIDATOR_SERVICE_URL", "wss://csec.nr-data.net") +_settings.security.detection.rci.enabled = _environ_as_bool("NEW_RELIC_SECURITY_DETECTION_RCI_ENABLED", True) +_settings.security.detection.rxss.enabled = _environ_as_bool("NEW_RELIC_SECURITY_DETECTION_RXSS_ENABLED", True) +_settings.security.detection.deserialization.enabled = _environ_as_bool( + "NEW_RELIC_SECURITY_DETECTION_DESERIALIZATION_ENABLED", True +) +_settings.security.request.body_limit = os.environ.get("NEW_RELIC_SECURITY_REQUEST_BODY_LIMIT", None) +_settings.security.scan_schedule.schedule = os.environ.get("NEW_RELIC_SECURITY_SCAN_SCHEDULE_SCHEDULE", None) +_settings.security.scan_schedule.duration = _environ_as_int("NEW_RELIC_SECURITY_SCAN_SCHEDULE_DURATION", -1) +_settings.security.scan_schedule.delay = _environ_as_int("NEW_RELIC_SECURITY_SCAN_SCHEDULE_DELAY", 0) +_settings.security.scan_schedule.always_sample_traces = _environ_as_bool( + "NEW_RELIC_SECURITY_SCAN_SCHEDULE_ALWAYS_SAMPLE_TRACES", False + ) +_settings.security.exclude_from_iast_scan.api = _environ_as_set( + "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_API", default="" +) +_settings.security.exclude_from_iast_scan.http_request_parameters.header = _environ_as_set( + "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_HTTP_REQUEST_PARAMETERS_HEADER", default="" +) +_settings.security.exclude_from_iast_scan.http_request_parameters.query = _environ_as_set( + "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_HTTP_REQUEST_PARAMETERS_QUERY", default="" +) +_settings.security.exclude_from_iast_scan.http_request_parameters.body = _environ_as_set( + "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_HTTP_REQUEST_PARAMETERS_BODY", default="" +) +_settings.security.exclude_from_iast_scan.iast_detection_category.insecure_settings = _environ_as_bool( + "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_INSECURE_SETTINGS", False +) +_settings.security.exclude_from_iast_scan.iast_detection_category.invalid_file_access = _environ_as_bool( + "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_INVALID_FILE_ACCESS", False +) +_settings.security.exclude_from_iast_scan.iast_detection_category.sql_injection = _environ_as_bool( + "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_SQL_INJECTION", False +) +_settings.security.exclude_from_iast_scan.iast_detection_category.nosql_injection = _environ_as_bool( + "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_NOSQL_INJECTION", False +) +_settings.security.exclude_from_iast_scan.iast_detection_category.ldap_injection = _environ_as_bool( + "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_LDAP_INJECTION", False +) +_settings.security.exclude_from_iast_scan.iast_detection_category.javascript_injection = _environ_as_bool( + "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_JAVASCRIPT_INJECTION", False +) +_settings.security.exclude_from_iast_scan.iast_detection_category.command_injection = _environ_as_bool( + "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_COMMAND_INJECTION", False +) +_settings.security.exclude_from_iast_scan.iast_detection_category.xpath_injection = _environ_as_bool( + "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_XPATH_INJECTION", False +) +_settings.security.exclude_from_iast_scan.iast_detection_category.ssrf = _environ_as_bool( + "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_SSRF", False +) +_settings.security.exclude_from_iast_scan.iast_detection_category.rxss = _environ_as_bool( + "NEW_RELIC_SECURITY_EXCLUDE_FROM_IAST_SCAN_IAST_DETECTION_CATEGORY_RXSS", False +) + def global_settings(): """This returns the default global settings. Generally only used diff --git a/newrelic/newrelic.ini b/newrelic/newrelic.ini index 967a448c4..29b57313c 100644 --- a/newrelic/newrelic.ini +++ b/newrelic/newrelic.ini @@ -49,6 +49,114 @@ app_name = Python Application # NEW_RELIC_MONITOR_MODE environment variable. monitor_mode = true +# Indicates if attack detection security module is to be enabled +security.enabled = false + +# To completely disable security set flag to false If the flag is +# set to false, the security module is not loaded. This property +# is read only once at application start. +security.agent.enabled = false + + +# security module provides two modes IAST or RASP +# RASP stands for Runtime Application Self Protection +# while IAST for Interactive Application Security Testing +# Default mode is IAST +security.mode = IAST + + +# web-protect agent endpoint connection URLs +security.validator_service_url = wss://csec.nr-data.net + + +# vulnerabilty detection flags +security.detection.rci.enabled = true +security.detection.rxss.enabled = true +security.detection.deserialization.enabled = true + + +# security request body read limiting in kb +security.request.body_limit = 300 + + +# Schedule IAST will allow users to schedule +# the startup and scanning of IAST. Users will have +# the flexibility to start and stop IAST at specific +# times or simply add a delay from the application +# start time + +# The schedule field specifies a cron expression that +# defines when the IAST scan should run. The cron +# expression consists of six fields separated by spaces: +# +# second: specifies the second of the hour (0-59) +# minute: specifies the minute of the hour (0-59) +# hour: specifies the hour of the day (0-23) +# day: specifies the day of the month (1-31) +# month: the month of the year (1-12) +# day_of_week: specifies the day of the week (0-6), where 0 = Sunday +# security.scan_schedule.schedule = 0 0 * * * ? + +# The duration field specifies the duration of +# the IAST scan in minutes. This determines how +# long the scan will run. +# Default value is forever i.e. no limits +# security.scan_schedule.duration = 300 + +# The delay field specifies the delay in +# minutes before the IAST scan starts. +# This allows you to schedule the scan to +# start at a later time. +# Default value: 0 minutes i.e. no delay +# security.scan_schedule.delay = 300 + +# This allows the newrelic security agent to collect +# sample data even when scan is not active. +# Default value is false. +# security.scan_schedule.always_sample_traces = false + +# The exclude_from_iast_scan configuration allows users to specify APIs, +# parameters, and categories that should not be scanned by Security Agents. + +# APIs can be specified using regular expression (regex) patterns that +# follow the syntax of Perl 5. The regex pattern should provide a complete +# match for the URL without the endpoint. +# security.exclude_from_iast_scan.api = .*\/api\/v1\/.*?\/login, .*\/api\/v2\/.*?\/login + +# The http_request_parameters configuration allows users to specify headers, query +# parameters, and body keys that should be excluded from IAST scans. + +# A list of HTTP header keys. If a request includes any headers with +# these keys, the corresponding IAST scan will be skipped. +# security.exclude_from_iast_scan.http_request_parameters.header = X-Forwaded-For, Set-Cookie + +# A list of query parameter keys. The presence of these parameters in +# the request's query string will lead to skipping the IAST scan. +# security.exclude_from_iast_scan.http_request_parameters.query = Query Parameter 1, Query Parameter 2 + +# A list of keys within the request body. If these keys are found in +# the body content, the IAST scan will be omitted. Supported content +# types for the request body include JSON, XML, and Form-URL-Encoded +# data. +# security.exclude_from_iast_scan.http_request_parameters.body = object.cc_number + +# The iast_detection_category configuration allows users to specify +# which categories of vulnerabilities should not be detected by +# Security Agents. If any of these categories are set to true, +# Security Agents will not generate events or flag vulnerabilities +# for that category. +security.exclude_from_iast_scan.iast_detection_category.insecure_settings = false +security.exclude_from_iast_scan.iast_detection_category.invalid_file_access = false +security.exclude_from_iast_scan.iast_detection_category.sql_injection = false +security.exclude_from_iast_scan.iast_detection_category.nosql_injection = false +security.exclude_from_iast_scan.iast_detection_category.ldap_injection = false +security.exclude_from_iast_scan.iast_detection_category.javascript_injection = false +security.exclude_from_iast_scan.iast_detection_category.command_injection = false +security.exclude_from_iast_scan.iast_detection_category.xpath_injection = false +security.exclude_from_iast_scan.iast_detection_category.ssrf = false +security.exclude_from_iast_scan.iast_detection_category.rxss = false + + # Sets the name of a file to log agent messages to. Whatever you # set this to, you must ensure that the permissions for the # containing directory and the file itself are correct, and @@ -251,5 +359,4 @@ monitor_mode = true [newrelic:production] monitor_mode = true - # --------------------------------------------------------------------------- diff --git a/setup.py b/setup.py index fdefc158d..e609940d9 100644 --- a/setup.py +++ b/setup.py @@ -181,7 +181,7 @@ def build_extension(self, ext): package_data={ "newrelic": ["newrelic.ini", "version.txt", "packages/urllib3/LICENSE.txt", "common/cacert.pem"], }, - extras_require={"infinite-tracing": ["grpcio", "protobuf"]}, + extras_require={"infinite-tracing": ["grpcio", "protobuf"], "iast": ["newrelic_security"]}, ) if with_setuptools: diff --git a/tests/framework_bottle/conftest.py b/tests/framework_bottle/conftest.py index 5e3098370..1e20551a5 100644 --- a/tests/framework_bottle/conftest.py +++ b/tests/framework_bottle/conftest.py @@ -25,6 +25,10 @@ "transaction_tracer.stack_trace_threshold": 0.0, "debug.log_data_collector_payloads": True, "debug.record_transaction_failure": True, + "security.agent.enabled": True, + "security.enabled": True, + "security.mode": "IAST", + "security.validator_service_url": "wss://csec-staging.nr-data.net" } collector_agent_registration = collector_agent_registration_fixture( diff --git a/tests/framework_django/conftest.py b/tests/framework_django/conftest.py index d7232bc02..bfc90d0e0 100644 --- a/tests/framework_django/conftest.py +++ b/tests/framework_django/conftest.py @@ -27,6 +27,10 @@ "debug.record_transaction_failure": True, "debug.log_autorum_middleware": True, "feature_flag": set(["django.instrumentation.inclusion-tags.r1"]), + "security.agent.enabled": True, + "security.enabled": True, + "security.mode": "IAST", + "security.validator_service_url": "wss://csec-staging.nr-data.net" } collector_agent_registration = collector_agent_registration_fixture( diff --git a/tests/framework_flask/conftest.py b/tests/framework_flask/conftest.py index 243b9b6ca..c52a11de9 100644 --- a/tests/framework_flask/conftest.py +++ b/tests/framework_flask/conftest.py @@ -41,6 +41,10 @@ "debug.log_data_collector_payloads": True, "debug.record_transaction_failure": True, "debug.log_autorum_middleware": True, + "security.agent.enabled": True, + "security.enabled": True, + "security.mode": "IAST", + "security.validator_service_url": "wss://csec-staging.nr-data.net" } collector_agent_registration = collector_agent_registration_fixture( diff --git a/tox.ini b/tox.ini index 56155c468..1533e831f 100644 --- a/tox.ini +++ b/tox.ini @@ -180,6 +180,8 @@ deps = coverage WebTest==3.0.0 py313: legacy-cgi==2.6.1 # cgi was removed from the stdlib in 3.13, and is required for WebTest + git+git@github.com:newrelic/csec-python-agent.git#egg=newrelic-security + # Test Suite Dependencies adapter_asgiref-asgireflatest: asgiref