Skip to content

Commit ff79486

Browse files
authored
Add support for API Security Custom Data Classification (#9710)
1 parent b281add commit ff79486

File tree

4 files changed

+219
-3
lines changed

4 files changed

+219
-3
lines changed

dd-java-agent/appsec/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ dependencies {
1515
implementation project(':internal-api')
1616
implementation project(':communication')
1717
implementation project(':telemetry')
18-
implementation group: 'io.sqreen', name: 'libsqreen', version: '17.1.0'
18+
implementation group: 'io.sqreen', name: 'libsqreen', version: '17.2.0'
1919
implementation libs.moshi
2020

2121
testImplementation libs.bytebuddy

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_ACTIVATION;
55
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE;
66
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE;
7+
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_CUSTOM_DATA_SCANNERS;
78
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_CUSTOM_RULES;
89
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_DD_MULTICONFIG;
910
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_DD_RULES;
@@ -13,6 +14,7 @@
1314
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_HEADER_FINGERPRINT;
1415
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_IP_BLOCKING;
1516
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_NETWORK_FINGERPRINT;
17+
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_PROCESSOR_OVERRIDES;
1618
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_CMDI;
1719
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_LFI;
1820
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SHI;
@@ -147,6 +149,8 @@ private long getRulesAndDataCapabilities() {
147149
| CAPABILITY_ASM_CUSTOM_RULES
148150
| CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE
149151
| CAPABILITY_ASM_TRUSTED_IPS
152+
| CAPABILITY_ASM_PROCESSOR_OVERRIDES
153+
| CAPABILITY_ASM_CUSTOM_DATA_SCANNERS
150154
| CAPABILITY_ENDPOINT_FINGERPRINT
151155
| CAPABILITY_ASM_SESSION_FINGERPRINT
152156
| CAPABILITY_ASM_NETWORK_FINGERPRINT
@@ -525,6 +529,8 @@ public void close() {
525529
| CAPABILITY_ASM_CUSTOM_RULES
526530
| CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE
527531
| CAPABILITY_ASM_TRUSTED_IPS
532+
| CAPABILITY_ASM_PROCESSOR_OVERRIDES
533+
| CAPABILITY_ASM_CUSTOM_DATA_SCANNERS
528534
| CAPABILITY_ASM_RASP_SQLI
529535
| CAPABILITY_ASM_RASP_SSRF
530536
| CAPABILITY_ASM_RASP_LFI

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,27 @@ import java.nio.file.Paths
2424
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_ACTIVATION
2525
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE
2626
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE
27+
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_CUSTOM_DATA_SCANNERS
2728
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_CUSTOM_RULES
29+
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_DD_MULTICONFIG
2830
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_DD_RULES
2931
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_EXCLUSIONS
3032
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_EXCLUSION_DATA
3133
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_HEADER_FINGERPRINT
3234
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_IP_BLOCKING
3335
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_NETWORK_FINGERPRINT
36+
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_PROCESSOR_OVERRIDES
3437
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_CMDI
3538
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_LFI
3639
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SHI
3740
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SQLI
3841
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SSRF
3942
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_REQUEST_BLOCKING
4043
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_SESSION_FINGERPRINT
44+
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_TRACE_TAGGING_RULES
4145
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_TRUSTED_IPS
4246
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_USER_BLOCKING
4347
import static datadog.remoteconfig.Capabilities.CAPABILITY_ENDPOINT_FINGERPRINT
44-
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_DD_MULTICONFIG
45-
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_TRACE_TAGGING_RULES
4648
import static datadog.remoteconfig.PollingHinterNoop.NOOP
4749
import static datadog.trace.api.UserIdCollectionMode.ANONYMIZATION
4850
import static datadog.trace.api.UserIdCollectionMode.DISABLED
@@ -282,6 +284,8 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
282284
| CAPABILITY_ASM_CUSTOM_RULES
283285
| CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE
284286
| CAPABILITY_ASM_TRUSTED_IPS
287+
| CAPABILITY_ASM_PROCESSOR_OVERRIDES
288+
| CAPABILITY_ASM_CUSTOM_DATA_SCANNERS
285289
| CAPABILITY_ASM_RASP_SQLI
286290
| CAPABILITY_ASM_RASP_SSRF
287291
| CAPABILITY_ASM_RASP_CMDI
@@ -442,6 +446,8 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
442446
| CAPABILITY_ASM_CUSTOM_RULES
443447
| CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE
444448
| CAPABILITY_ASM_TRUSTED_IPS
449+
| CAPABILITY_ASM_PROCESSOR_OVERRIDES
450+
| CAPABILITY_ASM_CUSTOM_DATA_SCANNERS
445451
| CAPABILITY_ENDPOINT_FINGERPRINT
446452
| CAPABILITY_ASM_SESSION_FINGERPRINT
447453
| CAPABILITY_ASM_NETWORK_FINGERPRINT
@@ -533,6 +539,8 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
533539
| CAPABILITY_ASM_CUSTOM_RULES
534540
| CAPABILITY_ASM_CUSTOM_BLOCKING_RESPONSE
535541
| CAPABILITY_ASM_TRUSTED_IPS
542+
| CAPABILITY_ASM_PROCESSOR_OVERRIDES
543+
| CAPABILITY_ASM_CUSTOM_DATA_SCANNERS
536544
| CAPABILITY_ASM_RASP_SQLI
537545
| CAPABILITY_ASM_RASP_SSRF
538546
| CAPABILITY_ASM_RASP_CMDI
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package datadog.smoketest.appsec
2+
3+
import datadog.trace.agent.test.utils.OkHttpUtils
4+
import okhttp3.FormBody
5+
import okhttp3.Request
6+
import spock.lang.Shared
7+
8+
/**
9+
* Smoke test to verify that processor_overrides and scanners configuration keys
10+
* are forwarded to libddwaf without alteration.
11+
*
12+
* This test creates a custom configuration with:
13+
* - A custom scanner that detects a specific pattern ("CUSTOM_TEST_PATTERN_12345")
14+
* - Processor overrides to include the custom scanner in schema extraction
15+
*
16+
* By verifying that the custom scanner triggers detection, we confirm that both
17+
* processor_overrides and scanners were correctly forwarded to libddwaf.
18+
*/
19+
class ProcessorOverridesSmokeTest extends AbstractAppSecServerSmokeTest {
20+
21+
@Shared
22+
String buildDir = new File(System.getProperty("datadog.smoketest.builddir")).absolutePath
23+
@Shared
24+
String customRulesPath = "${buildDir}/appsec_processor_overrides_rules.json"
25+
26+
def prepareCustomConfiguration() {
27+
// Prepare ruleset with custom scanners and processor overrides
28+
// This configuration will be passed to libddwaf and should be forwarded without modification
29+
30+
def customConfig = [
31+
version: "2.2",
32+
metadata: [
33+
rules_version: "1.0.0"
34+
],
35+
rules: [
36+
// Add a simple rule that triggers on custom scanner detection
37+
[
38+
id: 'custom-scanner-detection-rule',
39+
name: 'Custom Scanner Detection Test Rule',
40+
tags: [
41+
type: 'security_scanner',
42+
category: 'attack_attempt'
43+
],
44+
conditions: [
45+
[
46+
parameters: [
47+
inputs: [
48+
[
49+
address: 'server.request.body'
50+
]
51+
],
52+
regex: 'CUSTOM_TEST_PATTERN_12345'
53+
],
54+
operator: 'match_regex'
55+
]
56+
],
57+
transformers: [],
58+
on_match: []
59+
]
60+
],
61+
// Define custom scanners - this should be forwarded to libddwaf
62+
scanners: [
63+
[
64+
id: 'custom-test-scanner',
65+
name: 'Custom Test Scanner',
66+
key: [
67+
operator: 'match_regex',
68+
parameters: [
69+
regex: '.*'
70+
]
71+
],
72+
value: [
73+
operator: 'match_regex',
74+
parameters: [
75+
regex: 'CUSTOM_TEST_PATTERN_12345'
76+
]
77+
],
78+
tags: [
79+
type: 'custom_pattern',
80+
category: 'test'
81+
]
82+
]
83+
],
84+
// Define processor overrides - this should be forwarded to libddwaf
85+
processor_overrides: [
86+
[
87+
target: [[id: 'extract-schema']],
88+
scanners: [
89+
include: [[id: 'custom-test-scanner']],
90+
exclude: []
91+
]
92+
]
93+
]
94+
]
95+
96+
// Write the custom configuration to a file
97+
def gen = new groovy.json.JsonGenerator.Options().build()
98+
def configFile = new File(customRulesPath)
99+
configFile.text = gen.toJson(customConfig)
100+
101+
// Point the agent to use this custom configuration
102+
defaultAppSecProperties += "-Ddd.appsec.rules=${customRulesPath}" as String
103+
}
104+
105+
@Override
106+
ProcessBuilder createProcessBuilder() {
107+
// Prepare custom configuration before starting the process
108+
prepareCustomConfiguration()
109+
110+
String springBootShadowJar = System.getProperty("datadog.smoketest.appsec.springboot.shadowJar.path")
111+
112+
List<String> command = new ArrayList<>()
113+
command.add(javaPath())
114+
command.addAll(defaultJavaProperties)
115+
command.addAll(defaultAppSecProperties)
116+
command.addAll((String[]) ["-jar", springBootShadowJar, "--server.port=${httpPort}"])
117+
118+
ProcessBuilder processBuilder = new ProcessBuilder(command)
119+
processBuilder.directory(new File(buildDirectory))
120+
}
121+
122+
void 'test that custom scanner with processor_overrides is detected'() {
123+
given: 'a request containing the custom pattern'
124+
def url = "http://localhost:${httpPort}/greeting"
125+
def client = OkHttpUtils.clientBuilder().build()
126+
def formBuilder = new FormBody.Builder()
127+
formBuilder.add('test', 'CUSTOM_TEST_PATTERN_12345')
128+
def body = formBuilder.build()
129+
def request = new Request.Builder()
130+
.url(url)
131+
.post(body)
132+
.build()
133+
134+
when: 'the request is sent'
135+
def response = client.newCall(request).execute()
136+
137+
then: 'the response is successful'
138+
response.code() == 200
139+
140+
when: 'waiting for traces'
141+
waitForTraceCount(1)
142+
143+
then: 'the custom scanner rule is triggered'
144+
def rootSpans = this.rootSpans.toList()
145+
rootSpans.size() == 1
146+
def rootSpan = rootSpans[0]
147+
148+
// Verify that the custom rule was triggered, which confirms that:
149+
// 1. The 'scanners' configuration was forwarded to libddwaf
150+
// 2. The 'processor_overrides' configuration was forwarded to libddwaf
151+
// 3. Libddwaf correctly processed both configurations
152+
def trigger = null
153+
for (t in rootSpan.triggers) {
154+
if (t['rule']['id'] == 'custom-scanner-detection-rule') {
155+
trigger = t
156+
break
157+
}
158+
}
159+
assert trigger != null, 'Custom scanner rule was not triggered - configuration may not have been forwarded correctly'
160+
161+
// Verify the trigger contains expected information
162+
trigger['rule']['id'] == 'custom-scanner-detection-rule'
163+
trigger['rule']['tags']['type'] == 'security_scanner'
164+
trigger['rule']['tags']['category'] == 'attack_attempt'
165+
}
166+
167+
void 'test that custom scanner does not trigger without pattern'() {
168+
given: 'a request without the custom pattern'
169+
def url = "http://localhost:${httpPort}/greeting"
170+
def client = OkHttpUtils.clientBuilder().build()
171+
def formBuilder = new FormBody.Builder()
172+
formBuilder.add('test', 'normal_text_without_pattern')
173+
def body = formBuilder.build()
174+
def request = new Request.Builder()
175+
.url(url)
176+
.post(body)
177+
.build()
178+
179+
when: 'the request is sent'
180+
def response = client.newCall(request).execute()
181+
182+
then: 'the response is successful'
183+
response.code() == 200
184+
185+
when: 'waiting for traces'
186+
waitForTraceCount(1)
187+
188+
then: 'the custom scanner rule is NOT triggered'
189+
def rootSpans = this.rootSpans.toList()
190+
rootSpans.size() == 1
191+
def rootSpan = rootSpans[0]
192+
193+
def trigger = null
194+
for (t in rootSpan.triggers) {
195+
if (t['rule']['id'] == 'custom-scanner-detection-rule') {
196+
trigger = t
197+
break
198+
}
199+
}
200+
assert trigger == null, 'Custom scanner rule should not trigger without the pattern'
201+
}
202+
}

0 commit comments

Comments
 (0)