Skip to content

Commit c213055

Browse files
Add more tests
1 parent f84294e commit c213055

File tree

5 files changed

+157
-57
lines changed

5 files changed

+157
-57
lines changed

dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java

Lines changed: 54 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -102,11 +102,12 @@ public class AppSecConfigServiceImpl implements AppSecConfigService {
102102
private final AtomicBoolean subscribedToRulesAndData = new AtomicBoolean();
103103
private final Set<String> usedDDWafConfigKeys =
104104
Collections.newSetFromMap(new ConcurrentHashMap<>());
105-
private final Set<String> emptyConfigKeys = Collections.newSetFromMap(new ConcurrentHashMap<>());
105+
private final Set<String> ignoredConfigKeys =
106+
Collections.newSetFromMap(new ConcurrentHashMap<>());
106107
private final String DEFAULT_WAF_CONFIG_RULE = "DEFAULT_WAF_CONFIG";
107108
private String currentRuleVersion;
108109
private List<AppSecModule> modulesToUpdateVersionIn;
109-
private long rulesAndDataCapabilities;
110+
private long rulesAndDataCapabilities = -1L;
110111

111112
public AppSecConfigServiceImpl(
112113
Config tracerConfig,
@@ -135,32 +136,34 @@ private void subscribeConfigurationPoller() {
135136
}
136137

137138
private long buildRulesAndDataCapabilities() {
138-
long capabilities =
139-
CAPABILITY_ASM_DD_RULES
140-
| CAPABILITY_ASM_IP_BLOCKING
141-
| CAPABILITY_ASM_EXCLUSIONS
142-
| CAPABILITY_ASM_EXCLUSION_DATA
143-
| CAPABILITY_ASM_REQUEST_BLOCKING
144-
| CAPABILITY_ASM_USER_BLOCKING
145-
| CAPABILITY_ASM_CUSTOM_RULES
146-
| CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE
147-
| CAPABILITY_ASM_TRUSTED_IPS
148-
| CAPABILITY_ENDPOINT_FINGERPRINT
149-
| CAPABILITY_ASM_SESSION_FINGERPRINT
150-
| CAPABILITY_ASM_NETWORK_FINGERPRINT
151-
| CAPABILITY_ASM_HEADER_FINGERPRINT;
152-
if (tracerConfig.isAppSecRaspEnabled()) {
153-
capabilities |= CAPABILITY_ASM_RASP_SQLI;
154-
capabilities |= CAPABILITY_ASM_RASP_SSRF;
155-
capabilities |= CAPABILITY_ASM_RASP_CMDI;
156-
capabilities |= CAPABILITY_ASM_RASP_SHI;
157-
// RASP LFI is only available in fully enabled mode as it's implemented using callsite
158-
// instrumentation
159-
if (tracerConfig.getAppSecActivation() == ProductActivation.FULLY_ENABLED) {
160-
capabilities |= CAPABILITY_ASM_RASP_LFI;
139+
if (rulesAndDataCapabilities == -1) {
140+
rulesAndDataCapabilities =
141+
CAPABILITY_ASM_DD_RULES
142+
| CAPABILITY_ASM_IP_BLOCKING
143+
| CAPABILITY_ASM_EXCLUSIONS
144+
| CAPABILITY_ASM_EXCLUSION_DATA
145+
| CAPABILITY_ASM_REQUEST_BLOCKING
146+
| CAPABILITY_ASM_USER_BLOCKING
147+
| CAPABILITY_ASM_CUSTOM_RULES
148+
| CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE
149+
| CAPABILITY_ASM_TRUSTED_IPS
150+
| CAPABILITY_ENDPOINT_FINGERPRINT
151+
| CAPABILITY_ASM_SESSION_FINGERPRINT
152+
| CAPABILITY_ASM_NETWORK_FINGERPRINT
153+
| CAPABILITY_ASM_HEADER_FINGERPRINT;
154+
if (tracerConfig.isAppSecRaspEnabled()) {
155+
rulesAndDataCapabilities |= CAPABILITY_ASM_RASP_SQLI;
156+
rulesAndDataCapabilities |= CAPABILITY_ASM_RASP_SSRF;
157+
rulesAndDataCapabilities |= CAPABILITY_ASM_RASP_CMDI;
158+
rulesAndDataCapabilities |= CAPABILITY_ASM_RASP_SHI;
159+
// RASP LFI is only available in fully enabled mode as it's implemented using callsite
160+
// instrumentation
161+
if (tracerConfig.getAppSecActivation() == ProductActivation.FULLY_ENABLED) {
162+
rulesAndDataCapabilities |= CAPABILITY_ASM_RASP_LFI;
163+
}
161164
}
162165
}
163-
return capabilities;
166+
return rulesAndDataCapabilities;
164167
}
165168

166169
private void updateRulesAndDataSubscription() {
@@ -179,7 +182,7 @@ private void subscribeRulesAndData() {
179182
this.configurationPoller.addListener(Product.ASM_DD, new AppSecConfigChangesDDListener());
180183
this.configurationPoller.addListener(Product.ASM_DATA, new AppSecConfigChangesListener());
181184
this.configurationPoller.addListener(Product.ASM, new AppSecConfigChangesListener());
182-
this.configurationPoller.addCapabilities(rulesAndDataCapabilities);
185+
this.configurationPoller.addCapabilities(buildRulesAndDataCapabilities());
183186
}
184187
}
185188

@@ -188,7 +191,7 @@ private void unsubscribeRulesAndData() {
188191
this.configurationPoller.removeListeners(Product.ASM_DD);
189192
this.configurationPoller.removeListeners(Product.ASM_DATA);
190193
this.configurationPoller.removeListeners(Product.ASM);
191-
this.configurationPoller.removeCapabilities(rulesAndDataCapabilities);
194+
this.configurationPoller.removeCapabilities(buildRulesAndDataCapabilities());
192195
}
193196
}
194197

@@ -204,35 +207,39 @@ private class AppSecConfigChangesListener implements ProductListener {
204207
@Override
205208
public void accept(ConfigKey configKey, byte[] content, PollingRateHinter pollingRateHinter)
206209
throws IOException {
207-
maybeInitializeDefaultConfig();
208210
final String key = configKey.toString();
209211
if (content == null) {
210-
if (!emptyConfigKeys.remove(key)) {
211-
try {
212-
wafBuilder.removeConfig(key);
213-
} catch (UnclassifiedWafException e) {
214-
throw new RuntimeException(e);
215-
}
216-
}
212+
remove(configKey, pollingRateHinter);
213+
return;
214+
}
215+
Map<String, Object> contentMap =
216+
ADAPTER.fromJson(Okio.buffer(Okio.source(new ByteArrayInputStream(content))));
217+
if (contentMap == null || contentMap.isEmpty()) {
218+
ignoredConfigKeys.add(key);
217219
} else {
218-
Map<String, Object> contentMap =
219-
ADAPTER.fromJson(Okio.buffer(Okio.source(new ByteArrayInputStream(content))));
220-
if (contentMap == null || contentMap.isEmpty()) {
221-
emptyConfigKeys.add(key);
222-
} else {
223-
try {
224-
handleWafUpdateResultReport(key, contentMap);
225-
} catch (AppSecModule.AppSecModuleActivationException e) {
226-
throw new RuntimeException(e);
227-
}
220+
ignoredConfigKeys.remove(key);
221+
try {
222+
maybeInitializeDefaultConfig();
223+
handleWafUpdateResultReport(key, contentMap);
224+
} catch (AppSecModule.AppSecModuleActivationException e) {
225+
throw new RuntimeException(e);
228226
}
229227
}
230228
}
231229

232230
@Override
233231
public void remove(ConfigKey configKey, PollingRateHinter pollingRateHinter)
234232
throws IOException {
235-
accept(configKey, null, pollingRateHinter);
233+
final String key = configKey.toString();
234+
if (ignoredConfigKeys.remove(key)) {
235+
return;
236+
}
237+
try {
238+
maybeInitializeDefaultConfig();
239+
wafBuilder.removeConfig(key);
240+
} catch (UnclassifiedWafException e) {
241+
throw new RuntimeException(e);
242+
}
236243
}
237244

238245
@Override
@@ -375,7 +382,6 @@ public void init() {
375382
}
376383
this.mergedAsmFeatures.clear();
377384
this.usedDDWafConfigKeys.clear();
378-
this.emptyConfigKeys.clear();
379385
this.rulesAndDataCapabilities = buildRulesAndDataCapabilities();
380386

381387
if (wafConfig.isEmpty()) {

dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,6 +715,41 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
715715
service.usedDDWafConfigKeys.empty
716716
}
717717

718+
void 'test that empty configurations are acknowledged'() {
719+
given:
720+
final key = new ParsedConfigKey('Test', '1234', 1, 'ASM_DD', 'ID')
721+
722+
when:
723+
AppSecSystem.active = true
724+
config.getAppSecActivation() >> ProductActivation.FULLY_ENABLED
725+
final service = new AppSecConfigServiceImpl(config, poller, reconf)
726+
service.init()
727+
service.maybeSubscribeConfigPolling()
728+
729+
then:
730+
1 * poller.addListener(Product.ASM_DATA, _) >> {
731+
listeners.savedWafDataChangesListener = it[1]
732+
}
733+
1 * poller.addConfigurationEndListener(_) >> {
734+
listeners.savedConfEndListener = it[0]
735+
}
736+
737+
when:
738+
listeners.savedWafDataChangesListener.accept(key, '{}'.bytes, NOOP)
739+
listeners.savedConfEndListener.onConfigurationEnd()
740+
741+
then:
742+
noExceptionThrown()
743+
744+
when:
745+
listeners.savedWafDataChangesListener.accept(key, null, NOOP)
746+
listeners.savedConfEndListener.onConfigurationEnd()
747+
748+
then:
749+
noExceptionThrown()
750+
}
751+
752+
718753
private static AppSecFeatures autoUserInstrum(String mode) {
719754
return new AppSecFeatures().tap { features ->
720755
features.autoUserInstrum = new AppSecFeatures.AutoUserInstrum().tap { instrum ->

dd-smoke-tests/dynamic-config/src/main/java/datadog/smoketest/dynamicconfig/AppSecApplication.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
public class AppSecApplication {
66

7-
public static final long TIMEOUT_IN_SECONDS = 10;
7+
public static final long TIMEOUT_IN_SECONDS = 15;
88

99
public static void main(String[] args) throws InterruptedException {
1010
// just wait as we want to test RC payloads

dd-smoke-tests/dynamic-config/src/test/groovy/datadog/smoketest/AppSecActivationSmokeTest.groovy

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
package datadog.smoketest
22

3+
import datadog.remoteconfig.Capabilities
4+
import datadog.remoteconfig.Product
35
import datadog.smoketest.dynamicconfig.AppSecApplication
4-
import datadog.trace.test.util.Flaky
56

67
class AppSecActivationSmokeTest extends AbstractSmokeTest {
78

@@ -23,22 +24,55 @@ class AppSecActivationSmokeTest extends AbstractSmokeTest {
2324
processBuilder.directory(new File(buildDirectory))
2425
}
2526

26-
@Flaky
27-
void 'test activation config change is sent via RC'() {
28-
when:
27+
void 'test activation via RC workflow'() {
28+
given:
29+
final asmRuleProducts = [Product.ASM, Product.ASM_DD, Product.ASM_DATA]
30+
31+
when: 'appsec is enabled but inactive'
32+
final request = waitForRcClientRequest {req ->
33+
decodeProducts(req).find { asmRuleProducts.contains(it) } == null
34+
}
35+
final capabilities = decodeCapabilities(request)
36+
37+
then: 'only ASM_ACTIVATION capability should be reported'
38+
assert hasCapability(capabilities, Capabilities.CAPABILITY_ASM_ACTIVATION)
39+
assert !hasCapability(capabilities, Capabilities.CAPABILITY_ASM_CUSTOM_RULES)
40+
41+
when: 'appsec is enabled via RC'
2942
setRemoteConfig('datadog/2/ASM_FEATURES/asm_features_activation/config', '{"asm":{"enabled":true}}')
3043

31-
then:
44+
then: 'we should receive a product change for appsec'
3245
waitForTelemetryFlat {
33-
if (it['request_type'] != 'app-client-configuration-change') {
34-
return false
35-
}
3646
final configurations = (List<Map<String, Object>>) it?.payload?.configuration ?: []
3747
final enabledConfig = configurations.find { it.name == 'appsec_enabled' }
3848
if (!enabledConfig) {
3949
return false
4050
}
4151
return enabledConfig.value == 'true' && enabledConfig .origin == 'remote_config'
4252
}
53+
54+
and: 'we should have set the capabilities for ASM rules and data'
55+
final newRequest = waitForRcClientRequest {req ->
56+
decodeProducts(req).containsAll(asmRuleProducts)
57+
}
58+
final newCapabilities = decodeCapabilities(newRequest)
59+
assert hasCapability(newCapabilities, Capabilities.CAPABILITY_ASM_CUSTOM_RULES)
60+
}
61+
62+
private static Set<Product> decodeProducts(final Map<String, Object> request) {
63+
return request.client.products.collect { Product.valueOf(it)}
64+
}
65+
66+
private static long decodeCapabilities(final Map<String, Object> request) {
67+
final clientCapabilities = request.client.capabilities as byte[]
68+
long capabilities = 0l
69+
for (int i = 0; i < clientCapabilities.length; i++) {
70+
capabilities |= (clientCapabilities[i] & 0xFFL) << ((clientCapabilities.length - i - 1) * 8)
71+
}
72+
return capabilities
73+
}
74+
75+
private static boolean hasCapability(final long capabilities, final long test) {
76+
return (capabilities & test) > 0
4377
}
4478
}

dd-smoke-tests/src/main/groovy/datadog/smoketest/AbstractSmokeTest.groovy

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,12 @@ abstract class AbstractSmokeTest extends ProcessManager {
5050
@Shared
5151
protected TestHttpServer.Headers lastTraceRequestHeaders = null
5252

53+
@Shared
54+
protected CopyOnWriteArrayList<Map<String, Object>> rcClientMessages = new CopyOnWriteArrayList()
55+
56+
@Shared
57+
private Throwable rcClientDecodingFailure = null
58+
5359
@Shared
5460
protected final PollingConditions defaultPoll = new PollingConditions(timeout: 30, initialDelay: 0, delay: 1, factor: 1)
5561

@@ -129,6 +135,10 @@ abstract class AbstractSmokeTest extends ProcessManager {
129135
response.status(200).send()
130136
}
131137
prefix("/v0.7/config") {
138+
if (request.getBody() != null) {
139+
final msg = new JsonSlurper().parseText(new String(request.getBody(), StandardCharsets.UTF_8)) as Map<String, Object>
140+
rcClientMessages.add(msg)
141+
}
132142
response.status(200).send(remoteConfigResponse)
133143
}
134144
prefix("/telemetry/proxy/api/v2/apmtelemetry") {
@@ -349,6 +359,21 @@ abstract class AbstractSmokeTest extends ProcessManager {
349359
}
350360
}
351361

362+
Map<String, Object> waitForRcClientRequest(final Function<Map<String, Object>, Boolean> predicate) {
363+
waitForRcClientRequest(defaultPoll, predicate)
364+
}
365+
366+
Map<String, Object> waitForRcClientRequest(final PollingConditions poll, final Function<Map<String, Object>, Boolean> predicate) {
367+
def message = null
368+
poll.eventually {
369+
if (rcClientDecodingFailure != null) {
370+
throw rcClientDecodingFailure
371+
}
372+
assert (message = rcClientMessages.find { predicate.apply(it) }) != null
373+
}
374+
return message
375+
}
376+
352377
List<DecodedTrace> getTraces() {
353378
decodeTraces
354379
}

0 commit comments

Comments
 (0)