Skip to content

Commit 23ce5ef

Browse files
Add support for suspicious attacker blocking to appsec (#7401)
1 parent bb44d60 commit 23ce5ef

File tree

10 files changed

+316
-79
lines changed

10 files changed

+316
-79
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_CUSTOM_RULES;
99
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_DD_RULES;
1010
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_EXCLUSIONS;
11+
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_EXCLUSION_DATA;
1112
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_IP_BLOCKING;
1213
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SQLI;
1314
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_REQUEST_BLOCKING;
@@ -97,6 +98,7 @@ private void subscribeConfigurationPoller() {
9798
CAPABILITY_ASM_DD_RULES
9899
| CAPABILITY_ASM_IP_BLOCKING
99100
| CAPABILITY_ASM_EXCLUSIONS
101+
| CAPABILITY_ASM_EXCLUSION_DATA
100102
| CAPABILITY_ASM_REQUEST_BLOCKING
101103
| CAPABILITY_ASM_USER_BLOCKING
102104
| CAPABILITY_ASM_CUSTOM_RULES
@@ -335,6 +337,7 @@ public void close() {
335337
| CAPABILITY_ASM_DD_RULES
336338
| CAPABILITY_ASM_IP_BLOCKING
337339
| CAPABILITY_ASM_EXCLUSIONS
340+
| CAPABILITY_ASM_EXCLUSION_DATA
338341
| CAPABILITY_ASM_REQUEST_BLOCKING
339342
| CAPABILITY_ASM_USER_BLOCKING
340343
| CAPABILITY_ASM_CUSTOM_RULES
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package com.datadog.appsec.config;
2+
3+
import com.squareup.moshi.Json;
4+
import java.util.List;
5+
import java.util.Map;
6+
7+
public class AppSecData {
8+
9+
@Json(name = "rules_data")
10+
private List<Map<String, Object>> rules;
11+
12+
@Json(name = "exclusion_data")
13+
private List<Map<String, Object>> exclusion;
14+
15+
public List<Map<String, Object>> getRules() {
16+
return rules;
17+
}
18+
19+
public void setRules(List<Map<String, Object>> rules) {
20+
this.rules = rules;
21+
}
22+
23+
public List<Map<String, Object>> getExclusion() {
24+
return exclusion;
25+
}
26+
27+
public void setExclusion(List<Map<String, Object>> exclusion) {
28+
this.exclusion = exclusion;
29+
}
30+
}

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

Lines changed: 5 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,36 +3,25 @@
33
import static com.datadog.appsec.config.AppSecConfig.MOSHI;
44

55
import com.squareup.moshi.JsonAdapter;
6-
import com.squareup.moshi.Types;
76
import datadog.remoteconfig.ConfigurationDeserializer;
87
import java.io.ByteArrayInputStream;
98
import java.io.IOException;
109
import java.io.InputStream;
11-
import java.util.List;
12-
import java.util.Map;
1310
import okio.Okio;
1411

15-
public class AppSecDataDeserializer
16-
implements ConfigurationDeserializer<List<Map<String, Object>>> {
12+
public class AppSecDataDeserializer implements ConfigurationDeserializer<AppSecData> {
1713
public static final AppSecDataDeserializer INSTANCE = new AppSecDataDeserializer();
1814

19-
private static final JsonAdapter<Map<String, List<Map<String, Object>>>> ADAPTER =
20-
MOSHI.adapter(
21-
Types.newParameterizedType(
22-
Map.class,
23-
String.class,
24-
Types.newParameterizedType(
25-
List.class, Types.newParameterizedType(Map.class, String.class, Object.class))));
15+
private static final JsonAdapter<AppSecData> ADAPTER = MOSHI.adapter(AppSecData.class);
2616

2717
private AppSecDataDeserializer() {}
2818

2919
@Override
30-
public List<Map<String, Object>> deserialize(byte[] content) throws IOException {
20+
public AppSecData deserialize(byte[] content) throws IOException {
3121
return deserialize(new ByteArrayInputStream(content));
3222
}
3323

34-
private List<Map<String, Object>> deserialize(InputStream is) throws IOException {
35-
Map<String, List<Map<String, Object>>> cfg = ADAPTER.fromJson(Okio.buffer(Okio.source(is)));
36-
return cfg.get("rules_data");
24+
private AppSecData deserialize(InputStream is) throws IOException {
25+
return ADAPTER.fromJson(Okio.buffer(Okio.source(is)));
3726
}
3827
}

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

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,21 @@ public class CurrentAppSecConfig {
2424
MergedAsmData mergedAsmData = new MergedAsmData(new HashMap<>());
2525
public final DirtyStatus dirtyStatus = new DirtyStatus();
2626

27+
@SuppressWarnings("unchecked")
2728
public void setDdConfig(AppSecConfig newConfig) {
2829
this.ddConfig = newConfig;
2930

30-
List<Map<String, Object>> rulesData =
31-
(List<Map<String, Object>>) newConfig.getRawConfig().get("rules_data");
32-
if (rulesData != null) {
33-
mergedAsmData.addConfig(MergedAsmData.KEY_BUNDLED_RULE_DATA, rulesData);
31+
final Map<String, Object> rawConfig = newConfig.getRawConfig();
32+
List<Map<String, Object>> rules = (List<Map<String, Object>>) rawConfig.get("rules_data");
33+
List<Map<String, Object>> exclusions =
34+
(List<Map<String, Object>>) rawConfig.get("exclusion_data");
35+
if (rules != null || exclusions != null) {
36+
final AppSecData data = new AppSecData();
37+
data.setRules(rules);
38+
data.setExclusion(exclusions);
39+
mergedAsmData.addConfig(MergedAsmData.KEY_BUNDLED_DATA, data);
3440
} else {
35-
mergedAsmData.removeConfig(MergedAsmData.KEY_BUNDLED_RULE_DATA);
41+
mergedAsmData.removeConfig(MergedAsmData.KEY_BUNDLED_DATA);
3642
}
3743
}
3844

@@ -100,7 +106,9 @@ public AppSecConfig getMergedUpdateConfig() throws IOException {
100106
mso.put("rules_override", getMergedRuleOverrides());
101107
}
102108
if (dirtyStatus.data) {
103-
mso.put("rules_data", mergedAsmData.getMergedData());
109+
final AppSecData data = mergedAsmData.getMergedData();
110+
mso.put("rules_data", data.getRules());
111+
mso.put("exclusion_data", data.getExclusion());
104112
}
105113
if (dirtyStatus.actions) {
106114
mso.put("actions", getMergedActions());
@@ -111,12 +119,13 @@ public AppSecConfig getMergedUpdateConfig() throws IOException {
111119
if (log.isDebugEnabled()) {
112120
log.debug(
113121
"Providing WAF config with: "
114-
+ "rules: {}, custom_rules: {}, exclusions: {}, ruleOverrides: {}, rules_data: {}, actions: {}",
122+
+ "rules: {}, custom_rules: {}, exclusions: {}, ruleOverrides: {}, rules_data: {}, exclusion_data: {}, actions: {}",
115123
debugRuleSummary(mso),
116124
debugCustomRuleSummary(mso),
117125
debugExclusionsSummary(mso),
118126
debugRuleOverridesSummary(mso),
119127
debugRulesDataSummary(mso),
128+
debugExclusionDataSummary(mso),
120129
debugActionsSummary(mso));
121130
}
122131
return AppSecConfig.valueOf(mso);
@@ -141,13 +150,27 @@ private static String debugRulesDataSummary(Map<String, Object> mso) {
141150
}
142151
return "["
143152
+ rulesData.size()
144-
+ " data sets with ids "
153+
+ " rules data sets with ids "
145154
+ rulesData.stream()
146155
.map(rd -> String.valueOf(rd.get("id")))
147156
.collect(Collectors.joining(", "))
148157
+ "]";
149158
}
150159

160+
private static String debugExclusionDataSummary(Map<String, Object> mso) {
161+
List<Map<String, Object>> exclusionData = (List<Map<String, Object>>) mso.get("exclusion_data");
162+
if (exclusionData == null) {
163+
return "<absent>";
164+
}
165+
return "["
166+
+ exclusionData.size()
167+
+ " exclusion data sets with ids "
168+
+ exclusionData.stream()
169+
.map(rd -> String.valueOf(rd.get("id")))
170+
.collect(Collectors.joining(", "))
171+
+ "]";
172+
}
173+
151174
private static String debugRuleOverridesSummary(Map<String, Object> mso) {
152175
List<Map<String, Object>> overrides = (List<Map<String, Object>>) mso.get("rules_override");
153176
if (overrides == null) {

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

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,27 @@
33
import static java.util.stream.Collectors.groupingBy;
44
import static java.util.stream.Collectors.toList;
55

6-
import java.util.AbstractList;
6+
import java.util.Collection;
7+
import java.util.Collections;
78
import java.util.HashMap;
89
import java.util.List;
910
import java.util.Map;
1011
import java.util.Set;
12+
import java.util.function.Function;
1113
import java.util.stream.Collectors;
1214
import java.util.stream.Stream;
1315

14-
public class MergedAsmData extends AbstractList<Map<String, Object>> {
15-
public static final String KEY_BUNDLED_RULE_DATA = "<rule data bundled in rule config>";
16+
public class MergedAsmData {
17+
public static final String KEY_BUNDLED_DATA = "<data bundled in config>";
1618

17-
private final Map<String /* cfg key */, List<Map<String, Object>>> configs;
18-
private List<Map<String, Object>> mergedData;
19+
private final Map<String /* cfg key */, AppSecData> configs;
20+
private AppSecData mergedData;
1921

20-
public MergedAsmData(Map<String, List<Map<String, Object>>> configs) {
22+
public MergedAsmData(Map<String, AppSecData> configs) {
2123
this.configs = configs;
2224
}
2325

24-
public void addConfig(String cfgKey, List<Map<String, Object>> config) {
26+
public void addConfig(String cfgKey, AppSecData config) {
2527
this.configs.put(cfgKey, config);
2628
this.mergedData = null;
2729
}
@@ -40,19 +42,15 @@ public void removeConfig(String cfgKey) {
4042
* - value: 192.168.1.1
4143
* expiration: 555
4244
*/
43-
public List<Map<String, Object>> getMergedData() throws InvalidAsmDataException {
45+
public AppSecData getMergedData() throws InvalidAsmDataException {
4446
if (mergedData != null) {
4547
return mergedData;
4648
}
4749

4850
try {
49-
// map of id -> list of maps across all the configs with such id
50-
Map<String, List<Map<String, Object>>> dataPerId =
51-
configs.values().stream()
52-
.flatMap(l -> l.stream())
53-
.collect(groupingBy(d -> (String) d.get("id")));
54-
55-
this.mergedData = buildMergedData(dataPerId);
51+
this.mergedData = new AppSecData();
52+
this.mergedData.setRules(buildMergedData(groupById(AppSecData::getRules)));
53+
this.mergedData.setExclusion(buildMergedData(groupById(AppSecData::getExclusion)));
5654
} catch (InvalidAsmDataException iade) {
5755
throw iade;
5856
} catch (RuntimeException rte) {
@@ -62,6 +60,16 @@ public List<Map<String, Object>> getMergedData() throws InvalidAsmDataException
6260
return this.mergedData;
6361
}
6462

63+
/** map of id -> list of maps across all the configs with such id */
64+
private Map<String, List<Map<String, Object>>> groupById(
65+
final Function<AppSecData, List<Map<String, Object>>> property) {
66+
return configs.values().stream()
67+
.map(property)
68+
.map(it -> it == null ? Collections.<Map<String, Object>>emptyList() : it)
69+
.flatMap(Collection::stream)
70+
.collect(groupingBy(d -> (String) d.get("id")));
71+
}
72+
6573
private List<Map<String, Object>> buildMergedData(
6674
Map<String, List<Map<String, Object>>> dataPerId) {
6775
return dataPerId.entrySet().stream()
@@ -137,16 +145,6 @@ private List<Map<String, Object>> mergeExpirationData(Stream<Map<String, Object>
137145
.collect(toList());
138146
}
139147

140-
@Override
141-
public Map<String, Object> get(int index) {
142-
return getMergedData().get(index);
143-
}
144-
145-
@Override
146-
public int size() {
147-
return getMergedData().size();
148-
}
149-
150148
public class InvalidAsmDataException extends RuntimeException {
151149
public InvalidAsmDataException(String s) {
152150
super(s);

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

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_CUSTOM_BLOCKING_R
2222
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_CUSTOM_RULES
2323
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_DD_RULES
2424
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_EXCLUSIONS
25+
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_EXCLUSION_DATA
2526
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_IP_BLOCKING
2627
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SQLI
2728
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_REQUEST_BLOCKING
@@ -254,6 +255,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
254255
1 * poller.addCapabilities(CAPABILITY_ASM_DD_RULES
255256
| CAPABILITY_ASM_IP_BLOCKING
256257
| CAPABILITY_ASM_EXCLUSIONS
258+
| CAPABILITY_ASM_EXCLUSION_DATA
257259
| CAPABILITY_ASM_REQUEST_BLOCKING
258260
| CAPABILITY_ASM_USER_BLOCKING
259261
| CAPABILITY_ASM_CUSTOM_RULES
@@ -307,7 +309,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
307309
enabled : false
308310
]
309311
]
310-
casc.mergedAsmData == [[data: [], id: 'foo', type: '']]
312+
casc.mergedAsmData.mergedData.rules == [[data: [], id: 'foo', type: '']]
311313
}, _)
312314

313315
when:
@@ -398,6 +400,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
398400
1 * poller.addCapabilities(CAPABILITY_ASM_DD_RULES
399401
| CAPABILITY_ASM_IP_BLOCKING
400402
| CAPABILITY_ASM_EXCLUSIONS
403+
| CAPABILITY_ASM_EXCLUSION_DATA
401404
| CAPABILITY_ASM_REQUEST_BLOCKING
402405
| CAPABILITY_ASM_USER_BLOCKING
403406
| CAPABILITY_ASM_CUSTOM_RULES
@@ -430,7 +433,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
430433
}
431434
mergedUpdateConfig.numberOfRules == 0
432435
mergedUpdateConfig.rawConfig['rules_override'].isEmpty() == false
433-
mergedAsmData.isEmpty() == false
436+
mergedAsmData.mergedData.rules.isEmpty() == false
434437

435438
when:
436439
listeners.savedConfChangesListener.accept('asm_dd config', null, null)
@@ -447,7 +450,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
447450

448451
mergedUpdateConfig.numberOfRules > 0
449452
mergedUpdateConfig.rawConfig['rules_override'].isEmpty() == true
450-
mergedAsmData.isEmpty() == true
453+
mergedAsmData.mergedData.rules.isEmpty() == true
451454
}
452455

453456
void 'stopping appsec unsubscribes from the poller'() {
@@ -463,6 +466,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
463466
| CAPABILITY_ASM_DD_RULES
464467
| CAPABILITY_ASM_IP_BLOCKING
465468
| CAPABILITY_ASM_EXCLUSIONS
469+
| CAPABILITY_ASM_EXCLUSION_DATA
466470
| CAPABILITY_ASM_REQUEST_BLOCKING
467471
| CAPABILITY_ASM_USER_BLOCKING
468472
| CAPABILITY_ASM_CUSTOM_RULES

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

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class AppSecDataDeserializerSpecification extends Specification {
3939

4040
then:
4141
result != null
42-
result == [
42+
result.rules == [
4343
[
4444
data: [
4545
[
@@ -55,9 +55,44 @@ class AppSecDataDeserializerSpecification extends Specification {
5555
value : "3.3.3.3"
5656
]
5757
],
58-
id: "blocked_ips",
58+
id : "blocked_ips",
5959
type: "ip_with_expiration"
6060
]
6161
]
6262
}
63+
64+
void 'deserialize exclusions data'() {
65+
final deser = AppSecDataDeserializer.INSTANCE
66+
final input = """
67+
{
68+
"exclusion_data": [
69+
{
70+
"id": "suspicious_ips_data_id",
71+
"type": "ip_with_expiration",
72+
"data": [
73+
{
74+
"value": "34.65.27.85"
75+
}
76+
]
77+
}
78+
]
79+
}
80+
"""
81+
82+
when:
83+
def result = deser.deserialize(input.getBytes(StandardCharsets.UTF_8))
84+
85+
then:
86+
result != null
87+
result.exclusion == [
88+
[
89+
id : "suspicious_ips_data_id",
90+
type: "ip_with_expiration",
91+
data: [[
92+
value: "34.65.27.85"
93+
]],
94+
95+
]
96+
]
97+
}
6398
}

0 commit comments

Comments
 (0)