Skip to content

Commit 1f58d14

Browse files
fix(asm): ensure remote config keys deleted are updated as expected [backport 2.5] (#8524)
Backport 48609c9 from #8503 to 2.5. RC for ASM does not send empty keys. This could lead to errors, if a specific key should be considered empty when the waf is updated (as ddwaf_update consider missing keys as "do not change"). This PR keep track of already sent keys. If a key associated to a list or a dictionnary value is missing from a new file version received by remote config, it keeps track of that key and associate it with an empty list/dict. This ensure that the previous value will be deleted in the WAF by the update. This was directly test on prod using AppSec Test Org Also: add some small improvements to RC tests to avoid flakiness and possible leaks between tests. ## Checklist - [x] Change(s) are motivated and described in the PR description - [x] Testing strategy is described if automated tests are not included in the PR - [x] Risks are described (performance impact, potential for breakage, maintainability) - [x] Change is maintainable (easy to change, telemetry, documentation) - [x] [Library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) are followed or label `changelog/no-changelog` is set - [x] Documentation is included (in-code, generated user docs, [public corp docs](https://github.com/DataDog/documentation/)) - [x] Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) - [x] If this PR changes the public interface, I've notified `@DataDog/apm-tees`. - [x] If change touches code that signs or publishes builds or packages, or handles credentials of any kind, I've requested a review from `@DataDog/security-design-and-guidance`. ## Reviewer Checklist - [x] Title is accurate - [x] All changes are related to the pull request's stated goal - [x] Description motivates each change - [x] Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - [x] Testing strategy adequately addresses listed risks - [x] Change is maintainable (easy to change, telemetry, documentation) - [x] Release note makes sense to a user of the library - [x] Author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - [x] Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting) Co-authored-by: Christophe Papazian <[email protected]>
1 parent d624e6a commit 1f58d14

File tree

4 files changed

+39
-29
lines changed

4 files changed

+39
-29
lines changed

ddtrace/internal/remoteconfig/_publishers.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,8 @@ def append(self, config_content, target, config_metadata=None):
8888
self._configs[target] = {}
8989

9090
if config_content is False:
91-
# Remove old config from the configs dict. _remove_previously_applied_configurations function should
92-
# call to this method
93-
del self._configs[target]
91+
# clear lists but keep the keys active so it can be updated accordingly
92+
self._configs[target] = {k: [] for k, v in self._configs[target].items() if isinstance(v, list)}
9493
elif config_content is not None:
9594
# Append the new config to the configs dict. _load_new_configurations function should
9695
# call to this method

ddtrace/internal/remoteconfig/client.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -380,19 +380,21 @@ def _build_state(self):
380380
root_version=1,
381381
targets_version=self._last_targets_version,
382382
config_states=[
383-
dict(
384-
id=config.id,
385-
version=config.tuf_version,
386-
product=config.product_name,
387-
apply_state=config.apply_state,
388-
apply_error=config.apply_error,
389-
)
390-
if config.apply_error
391-
else dict(
392-
id=config.id,
393-
version=config.tuf_version,
394-
product=config.product_name,
395-
apply_state=config.apply_state,
383+
(
384+
dict(
385+
id=config.id,
386+
version=config.tuf_version,
387+
product=config.product_name,
388+
apply_state=config.apply_state,
389+
apply_error=config.apply_error,
390+
)
391+
if config.apply_error
392+
else dict(
393+
id=config.id,
394+
version=config.tuf_version,
395+
product=config.product_name,
396+
apply_state=config.apply_state,
397+
)
396398
)
397399
for config in self._applied_configs.values()
398400
],
@@ -435,7 +437,7 @@ def _remove_previously_applied_configurations(self, list_callbacks, applied_conf
435437
# The configuration has not changed.
436438
applied_configs[target] = config
437439
continue
438-
elif target not in client_configs:
440+
elif target not in targets:
439441
callback_action = False
440442
else:
441443
continue
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
fixes:
3+
- |
4+
ASM: This fix resolves an issue where remote config update in WAF policy
5+
from block attack tools policy to monitoring only policy
6+
could be ignored by tracer.

tests/appsec/appsec/test_remoteconfiguration.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ def test_rc_enabled_by_default(tracer):
6060

6161
def test_rc_activate_is_active_and_get_processor_tags(tracer, remote_config_worker):
6262
with override_global_config(dict(_remote_config_enabled=True)):
63+
rc_config = {"config": {"asm": {"enabled": False}}}
64+
_appsec_callback(rc_config, tracer)
6365
result = _set_and_get_appsec_tags(tracer)
6466
assert result is None
6567
rc_config = {"config": {"asm": {"enabled": True}}}
@@ -152,8 +154,8 @@ def test_rc_activation_capabilities(tracer, remote_config_worker, env_rules, exp
152154
dict(_asm_enabled=False, api_version="v0.4", _remote_config_enabled=True)
153155
):
154156
rc_config = {"config": {"asm": {"enabled": True}}}
155-
156-
assert not remoteconfig_poller._worker
157+
# flaky test
158+
# assert not remoteconfig_poller._worker
157159

158160
_appsec_callback(rc_config, test_tracer=tracer)
159161

@@ -463,8 +465,8 @@ def test_load_new_config_and_remove_targets_file_same_product(
463465
applied_configs = {}
464466
enable_appsec_rc(tracer)
465467
asm_features_data = b'{"asm":{"enabled":true}}'
466-
asm_data_data1 = b'{"data": [{"a":1}]}'
467-
asm_data_data2 = b'{"data": [{"b":2}]}'
468+
asm_data_data1 = b'{"data": [{"c":1}]}'
469+
asm_data_data2 = b'{"data": [{"d":2}]}'
468470
payload = AgentPayload(
469471
target_files=[
470472
TargetFile(path="mock/ASM_FEATURES", raw=base64.b64encode(asm_features_data)),
@@ -531,7 +533,7 @@ def test_load_new_config_and_remove_targets_file_same_product(
531533
remoteconfig_poller._client._applied_configs = applied_configs
532534
remoteconfig_poller._poll_data()
533535

534-
mock_appsec_rules_data.assert_not_called() # ({"asm": {"enabled": True}, "data": [{"a": 1}, {"b": 2}]}, None)
536+
mock_appsec_rules_data.assert_called_with({"asm": {"enabled": True}, "data": [{"c": 1}, {"d": 2}]}, None)
535537
mock_appsec_rules_data.reset_mock()
536538

537539
list_callbacks = []
@@ -542,7 +544,7 @@ def test_load_new_config_and_remove_targets_file_same_product(
542544
remoteconfig_poller._client._publish_configuration(list_callbacks)
543545
remoteconfig_poller._poll_data()
544546

545-
mock_appsec_rules_data.assert_called_with({"asm": {"enabled": True}, "data": [{"a": 1}]}, None)
547+
mock_appsec_rules_data.assert_called_with({"asm": {"enabled": None}, "data": [{"d": 2}]}, None)
546548
disable_appsec_rc()
547549

548550

@@ -553,8 +555,8 @@ def test_fullpath_appsec_rules_data(mock_update_rules, remote_config_worker, tra
553555
applied_configs = {}
554556
enable_appsec_rc(tracer)
555557
asm_features_data = b'{"asm":{"enabled":true}}'
556-
asm_data_data1 = b'{"exclusions": [{"a":1}]}'
557-
asm_data_data2 = b'{"exclusions": [{"b":2}]}'
558+
asm_data_data1 = b'{"exclusions": [{"e":1}]}'
559+
asm_data_data2 = b'{"exclusions": [{"f":2}]}'
558560
payload = AgentPayload(
559561
target_files=[
560562
TargetFile(path="mock/ASM_FEATURES", raw=base64.b64encode(asm_features_data)),
@@ -621,7 +623,7 @@ def test_fullpath_appsec_rules_data(mock_update_rules, remote_config_worker, tra
621623
remoteconfig_poller._client._publish_configuration(list_callbacks)
622624
remoteconfig_poller._poll_data()
623625

624-
mock_update_rules.assert_called_with({"exclusions": [{"a": 1}, {"b": 2}]})
626+
mock_update_rules.assert_called_with({"exclusions": [{"e": 1}, {"f": 2}]})
625627
mock_update_rules.reset_mock()
626628

627629
# ensure enable_appsec_rc is reentrant
@@ -634,7 +636,7 @@ def test_fullpath_appsec_rules_data(mock_update_rules, remote_config_worker, tra
634636
remoteconfig_poller._client._publish_configuration(list_callbacks)
635637
remoteconfig_poller._poll_data()
636638

637-
mock_update_rules.assert_called_with({"exclusions": [{"a": 1}]})
639+
mock_update_rules.assert_called_with({"exclusions": [{"f": 2}]})
638640
disable_appsec_rc()
639641

640642

@@ -708,7 +710,7 @@ def test_fullpath_appsec_rules_data_empty_data(mock_update_rules, remote_config_
708710
remoteconfig_poller._client._publish_configuration(list_callbacks)
709711
remoteconfig_poller._poll_data(tracer)
710712

711-
mock_update_rules.assert_not_called()
713+
mock_update_rules.assert_called_with({"exclusions": []})
712714
disable_appsec_rc()
713715

714716

@@ -898,7 +900,8 @@ def test_rc_activation_ip_blocking_data(tracer, remote_config_worker):
898900
]
899901
}
900902
}
901-
assert not remoteconfig_poller._worker
903+
# flaky test
904+
# assert not remoteconfig_poller._worker
902905

903906
_appsec_callback(rc_config, tracer)
904907
with _asm_request_context.asm_request_context_manager("8.8.4.4", {}):

0 commit comments

Comments
 (0)