Skip to content

Commit 2701dc6

Browse files
github-actions[bot]christophe-papazianemmettbutler
authored
fix(asm): ensure remote config keys deleted are updated as expected [backport 1.20] (#8522)
Backport 48609c9 from #8503 to 1.20. 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]> Co-authored-by: Christophe Papazian <[email protected]> Co-authored-by: Emmett Butler <[email protected]> Co-authored-by: Emmett Butler <[email protected]>
1 parent 0ac63ae commit 2701dc6

File tree

6 files changed

+41
-29
lines changed

6 files changed

+41
-29
lines changed

.github/workflows/test_frameworks.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,6 +302,8 @@ jobs:
302302
python-version: '3.8'
303303
- name: Install tox
304304
if: needs.needs-run.outputs.outcome == 'success'
305+
run: pip install envier
306+
run: pip install "pytest<8.1"
305307
run: pip install tox
306308
- name: Create tox env
307309
if: needs.needs-run.outputs.outcome == 'success'
1.37 MB
Binary file not shown.

ddtrace/internal/remoteconfig/_publishers.py

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

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

ddtrace/internal/remoteconfig/client.py

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -355,19 +355,21 @@ def _build_state(self):
355355
root_version=1,
356356
targets_version=self._last_targets_version,
357357
config_states=[
358-
dict(
359-
id=config.id,
360-
version=config.tuf_version,
361-
product=config.product_name,
362-
apply_state=config.apply_state,
363-
apply_error=config.apply_error,
364-
)
365-
if config.apply_error
366-
else dict(
367-
id=config.id,
368-
version=config.tuf_version,
369-
product=config.product_name,
370-
apply_state=config.apply_state,
358+
(
359+
dict(
360+
id=config.id,
361+
version=config.tuf_version,
362+
product=config.product_name,
363+
apply_state=config.apply_state,
364+
apply_error=config.apply_error,
365+
)
366+
if config.apply_error
367+
else dict(
368+
id=config.id,
369+
version=config.tuf_version,
370+
product=config.product_name,
371+
apply_state=config.apply_state,
372+
)
371373
)
372374
for config in self._applied_configs.values()
373375
],
@@ -410,7 +412,7 @@ def _remove_previously_applied_configurations(self, list_callbacks, applied_conf
410412
# The configuration has not changed.
411413
applied_configs[target] = config
412414
continue
413-
elif target not in client_configs:
415+
elif target not in targets:
414416
callback_action = False
415417
else:
416418
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/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(_appsec_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

@@ -450,8 +452,8 @@ def test_load_new_config_and_remove_targets_file_same_product(
450452
applied_configs = {}
451453
enable_appsec_rc(tracer)
452454
asm_features_data = b'{"asm":{"enabled":true}}'
453-
asm_data_data1 = b'{"data": [{"a":1}]}'
454-
asm_data_data2 = b'{"data": [{"b":2}]}'
455+
asm_data_data1 = b'{"data": [{"c":1}]}'
456+
asm_data_data2 = b'{"data": [{"d":2}]}'
455457
payload = AgentPayload(
456458
target_files=[
457459
TargetFile(path="mock/ASM_FEATURES", raw=base64.b64encode(asm_features_data)),
@@ -518,7 +520,7 @@ def test_load_new_config_and_remove_targets_file_same_product(
518520
remoteconfig_poller._client._applied_configs = applied_configs
519521
remoteconfig_poller._poll_data()
520522

521-
mock_appsec_rules_data.assert_not_called() # ({"asm": {"enabled": True}, "data": [{"a": 1}, {"b": 2}]}, None)
523+
mock_appsec_rules_data.assert_called_with({"asm": {"enabled": True}, "data": [{"c": 1}, {"d": 2}]}, None)
522524
mock_appsec_rules_data.reset_mock()
523525

524526
list_callbacks = []
@@ -529,7 +531,7 @@ def test_load_new_config_and_remove_targets_file_same_product(
529531
remoteconfig_poller._client._publish_configuration(list_callbacks)
530532
remoteconfig_poller._poll_data()
531533

532-
mock_appsec_rules_data.assert_called_with({"asm": {"enabled": True}, "data": [{"a": 1}]}, None)
534+
mock_appsec_rules_data.assert_called_with({"asm": {"enabled": None}, "data": [{"d": 2}]}, None)
533535
disable_appsec_rc()
534536

535537

@@ -541,8 +543,8 @@ def test_fullpath_appsec_rules_data(mock_update_rules, remote_config_worker, tra
541543
applied_configs = {}
542544
enable_appsec_rc(tracer)
543545
asm_features_data = b'{"asm":{"enabled":true}}'
544-
asm_data_data1 = b'{"exclusions": [{"a":1}]}'
545-
asm_data_data2 = b'{"exclusions": [{"b":2}]}'
546+
asm_data_data1 = b'{"exclusions": [{"e":1}]}'
547+
asm_data_data2 = b'{"exclusions": [{"f":2}]}'
546548
payload = AgentPayload(
547549
target_files=[
548550
TargetFile(path="mock/ASM_FEATURES", raw=base64.b64encode(asm_features_data)),
@@ -609,7 +611,7 @@ def test_fullpath_appsec_rules_data(mock_update_rules, remote_config_worker, tra
609611
remoteconfig_poller._client._publish_configuration(list_callbacks)
610612
remoteconfig_poller._poll_data()
611613

612-
mock_update_rules.assert_called_with({"exclusions": [{"a": 1}, {"b": 2}]})
614+
mock_update_rules.assert_called_with({"exclusions": [{"e": 1}, {"f": 2}]})
613615
mock_update_rules.reset_mock()
614616

615617
# ensure enable_appsec_rc is reentrant
@@ -622,7 +624,7 @@ def test_fullpath_appsec_rules_data(mock_update_rules, remote_config_worker, tra
622624
remoteconfig_poller._client._publish_configuration(list_callbacks)
623625
remoteconfig_poller._poll_data()
624626

625-
mock_update_rules.assert_called_with({"exclusions": [{"a": 1}]})
627+
mock_update_rules.assert_called_with({"exclusions": [{"f": 2}]})
626628
disable_appsec_rc()
627629

628630

@@ -697,7 +699,7 @@ def test_fullpath_appsec_rules_data_empty_data(mock_update_rules, remote_config_
697699
remoteconfig_poller._client._publish_configuration(list_callbacks)
698700
remoteconfig_poller._poll_data(tracer)
699701

700-
mock_update_rules.assert_not_called()
702+
mock_update_rules.assert_called_with({"exclusions": []})
701703
disable_appsec_rc()
702704

703705

@@ -887,7 +889,8 @@ def test_rc_activation_ip_blocking_data(tracer, remote_config_worker):
887889
]
888890
}
889891
}
890-
assert not remoteconfig_poller._worker
892+
# flaky test
893+
# assert not remoteconfig_poller._worker
891894

892895
_appsec_callback(rc_config, tracer)
893896
with _asm_request_context.asm_request_context_manager("8.8.4.4", {}):

0 commit comments

Comments
 (0)