Skip to content

Commit c542c6b

Browse files
committed
[appsec] Add Remote Config subscription for ASM_SCA product
Implements Remote Config infrastructure for Supply Chain Analysis (SCA) vulnerability detection via dynamic instrumentation. This commit adds: - New Product.ASM_SCA enum value for Remote Config product type - CAPABILITY_ASM_SCA_VULNERABILITY_DETECTION capability flag (bit 47) - AppSecSCAConfig data model with InstrumentationTarget (className/methodName) - AppSecSCAConfigDeserializer for JSON deserialization - subscribeSCA()/unsubscribeSCA() lifecycle methods in AppSecConfigServiceImpl - Integration into Remote Config subscription/cleanup flows The subscription stores incoming SCA configs in currentSCAConfig field. Actual bytecode retransformation will be implemented in future commits when AppSecInstrumentationUpdater is added.
1 parent 17c7fcf commit c542c6b

File tree

8 files changed

+388
-0
lines changed

8 files changed

+388
-0
lines changed

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

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SQLI;
2222
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_RASP_SSRF;
2323
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_REQUEST_BLOCKING;
24+
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_SCA_VULNERABILITY_DETECTION;
2425
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_SESSION_FINGERPRINT;
2526
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_TRACE_TAGGING_RULES;
2627
import static datadog.remoteconfig.Capabilities.CAPABILITY_ASM_TRUSTED_IPS;
@@ -103,13 +104,15 @@ public class AppSecConfigServiceImpl implements AppSecConfigService {
103104
private boolean hasUserWafConfig;
104105
private boolean defaultConfigActivated;
105106
private final AtomicBoolean subscribedToRulesAndData = new AtomicBoolean();
107+
private final AtomicBoolean subscribedToSCA = new AtomicBoolean();
106108
private final Set<String> usedDDWafConfigKeys =
107109
Collections.newSetFromMap(new ConcurrentHashMap<>());
108110
private final Set<String> ignoredConfigKeys =
109111
Collections.newSetFromMap(new ConcurrentHashMap<>());
110112
private final String DEFAULT_WAF_CONFIG_RULE = "ASM_DD/default";
111113
private String currentRuleVersion;
112114
private List<AppSecModule> modulesToUpdateVersionIn;
115+
private volatile AppSecSCAConfig currentSCAConfig;
113116

114117
public AppSecConfigServiceImpl(
115118
Config tracerConfig,
@@ -134,6 +137,8 @@ private void subscribeConfigurationPoller() {
134137
log.debug("Will not subscribe to ASM, ASM_DD and ASM_DATA (AppSec custom rules in use)");
135138
}
136139

140+
subscribeSCA();
141+
137142
this.configurationPoller.addConfigurationEndListener(applyRemoteConfigListener);
138143
}
139144

@@ -345,6 +350,51 @@ private void subscribeAsmFeatures() {
345350
this.configurationPoller.addCapabilities(CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE);
346351
}
347352

353+
/**
354+
* Subscribes to Supply Chain Analysis (SCA) configuration from Remote Config.
355+
* Receives instrumentation targets for vulnerability detection in third-party dependencies.
356+
*/
357+
private void subscribeSCA() {
358+
if (subscribedToSCA.compareAndSet(false, true)) {
359+
log.debug("Subscribing to ASM_SCA Remote Config product");
360+
this.configurationPoller.addListener(
361+
Product.ASM_SCA,
362+
AppSecSCAConfigDeserializer.INSTANCE,
363+
(configKey, newConfig, hinter) -> {
364+
if (newConfig == null) {
365+
log.debug("Received removal for SCA config key: {}", configKey);
366+
currentSCAConfig = null;
367+
// TODO: Trigger retransformation to remove instrumentation when updater exists
368+
} else {
369+
log.debug(
370+
"Received SCA config update for key: {} - enabled: {}, targets: {}",
371+
configKey,
372+
newConfig.enabled,
373+
newConfig.instrumentationTargets != null
374+
? newConfig.instrumentationTargets.size()
375+
: 0);
376+
currentSCAConfig = newConfig;
377+
// TODO: Trigger retransformation when AppSecInstrumentationUpdater exists
378+
}
379+
});
380+
this.configurationPoller.addCapabilities(CAPABILITY_ASM_SCA_VULNERABILITY_DETECTION);
381+
log.info("Successfully subscribed to ASM_SCA Remote Config product");
382+
}
383+
}
384+
385+
/**
386+
* Unsubscribes from SCA Remote Config product and clears current configuration.
387+
*/
388+
private void unsubscribeSCA() {
389+
if (subscribedToSCA.compareAndSet(true, false)) {
390+
log.debug("Unsubscribing from ASM_SCA Remote Config product");
391+
this.configurationPoller.removeListeners(Product.ASM_SCA);
392+
this.configurationPoller.removeCapabilities(CAPABILITY_ASM_SCA_VULNERABILITY_DETECTION);
393+
currentSCAConfig = null;
394+
log.info("Successfully unsubscribed from ASM_SCA Remote Config product");
395+
}
396+
}
397+
348398
private void distributeSubConfigurations(
349399
String key, AppSecModuleConfigurer.Reconfiguration reconfiguration) {
350400
maybeInitializeDefaultConfig();
@@ -547,6 +597,7 @@ public void close() {
547597
this.configurationPoller.removeListeners(Product.ASM_DATA);
548598
this.configurationPoller.removeListeners(Product.ASM);
549599
this.configurationPoller.removeListeners(Product.ASM_FEATURES);
600+
unsubscribeSCA();
550601
this.configurationPoller.removeConfigurationEndListener(applyRemoteConfigListener);
551602
this.subscribedToRulesAndData.set(false);
552603
this.configurationPoller.stop();
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package com.datadog.appsec.config;
2+
3+
import com.squareup.moshi.Json;
4+
import java.util.List;
5+
6+
/**
7+
* Configuration model for Supply Chain Analysis (SCA) vulnerability detection.
8+
* Received via Remote Config in the ASM_SCA product.
9+
*
10+
* <p>This configuration enables dynamic instrumentation of third-party dependencies
11+
* to detect and report known vulnerabilities at runtime.
12+
*/
13+
public class AppSecSCAConfig {
14+
15+
/**
16+
* Whether SCA vulnerability detection is enabled.
17+
*/
18+
@Json(name = "enabled")
19+
public Boolean enabled;
20+
21+
/**
22+
* List of instrumentation targets for SCA analysis.
23+
* Each target specifies a class/method to instrument for vulnerability detection.
24+
*/
25+
@Json(name = "instrumentation_targets")
26+
public List<InstrumentationTarget> instrumentationTargets;
27+
28+
/**
29+
* Represents a single instrumentation target for SCA.
30+
*/
31+
public static class InstrumentationTarget {
32+
/**
33+
* Fully qualified class name in internal format (e.g., "org/springframework/web/client/RestTemplate").
34+
*/
35+
@Json(name = "class_name")
36+
public String className;
37+
38+
/**
39+
* Method name to instrument (e.g., "execute").
40+
*/
41+
@Json(name = "method_name")
42+
public String methodName;
43+
}
44+
}
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.JsonAdapter;
4+
import com.squareup.moshi.Moshi;
5+
import datadog.remoteconfig.ConfigurationDeserializer;
6+
import okio.Okio;
7+
import java.io.ByteArrayInputStream;
8+
import java.io.IOException;
9+
10+
/**
11+
* Deserializer for Supply Chain Analysis (SCA) configuration from Remote Config.
12+
* Converts JSON payload from ASM_SCA product into typed AppSecSCAConfig objects.
13+
*/
14+
public class AppSecSCAConfigDeserializer implements ConfigurationDeserializer<AppSecSCAConfig> {
15+
16+
public static final AppSecSCAConfigDeserializer INSTANCE = new AppSecSCAConfigDeserializer();
17+
18+
private static final JsonAdapter<AppSecSCAConfig> ADAPTER =
19+
new Moshi.Builder().build().adapter(AppSecSCAConfig.class);
20+
21+
private AppSecSCAConfigDeserializer() {}
22+
23+
@Override
24+
public AppSecSCAConfig deserialize(byte[] content) throws IOException {
25+
if (content == null || content.length == 0) {
26+
return null;
27+
}
28+
return ADAPTER.fromJson(Okio.buffer(Okio.source(new ByteArrayInputStream(content))));
29+
}
30+
}

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

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
9999
1 * poller.addListener(Product.ASM_FEATURES, _, _)
100100
1 * poller.addListener(Product.ASM, _)
101101
1 * poller.addListener(Product.ASM_DATA, _)
102+
1 * poller.addListener(Product.ASM_SCA, _, _)
102103
1 * poller.addConfigurationEndListener(_)
103104
0 * poller.addListener(*_)
104105
0 * poller.addCapabilities(CAPABILITY_ASM_ACTIVATION)
@@ -135,6 +136,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
135136
then:
136137
2 * config.getAppSecActivation() >> ProductActivation.ENABLED_INACTIVE
137138
1 * poller.addListener(Product.ASM_FEATURES, _, _)
139+
1 * poller.addListener(Product.ASM_SCA, _, _)
138140
1 * poller.addConfigurationEndListener(_)
139141
0 * poller.addListener(*_)
140142
}
@@ -211,11 +213,13 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
211213
listeners.savedFeaturesDeserializer = it[1]
212214
listeners.savedFeaturesListener = it[2]
213215
}
216+
1 * poller.addListener(Product.ASM_SCA, _, _)
214217
1 * poller.addConfigurationEndListener(_) >> {
215218
listeners.savedConfEndListener = it[0]
216219
}
217220
1 * poller.addCapabilities(CAPABILITY_ASM_ACTIVATION)
218221
1 * poller.addCapabilities(CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE)
222+
1 * poller.addCapabilities({ it & datadog.remoteconfig.Capabilities.CAPABILITY_ASM_SCA_VULNERABILITY_DETECTION })
219223
0 * poller._
220224

221225
when:
@@ -252,11 +256,13 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
252256
listeners.savedFeaturesDeserializer = it[1]
253257
listeners.savedFeaturesListener = it[2]
254258
}
259+
1 * poller.addListener(Product.ASM_SCA, _, _)
255260
1 * poller.addConfigurationEndListener(_) >> {
256261
listeners.savedConfEndListener = it[0]
257262
}
258263
1 * poller.addCapabilities(CAPABILITY_ASM_ACTIVATION)
259264
1 * poller.addCapabilities(CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE)
265+
1 * poller.addCapabilities({ it & datadog.remoteconfig.Capabilities.CAPABILITY_ASM_SCA_VULNERABILITY_DETECTION })
260266
0 * poller._
261267

262268
when:
@@ -416,11 +422,13 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
416422
listeners.savedFeaturesDeserializer = it[1]
417423
listeners.savedFeaturesListener = it[2]
418424
}
425+
1 * poller.addListener(Product.ASM_SCA, _, _)
419426
1 * poller.addConfigurationEndListener(_) >> {
420427
listeners.savedConfEndListener = it[0]
421428
}
422429
1 * poller.addCapabilities(CAPABILITY_ASM_ACTIVATION)
423430
1 * poller.addCapabilities(CAPABILITY_ASM_AUTO_USER_INSTRUM_MODE)
431+
1 * poller.addCapabilities({ it & datadog.remoteconfig.Capabilities.CAPABILITY_ASM_SCA_VULNERABILITY_DETECTION })
424432
0 * poller._
425433

426434
when:
@@ -553,6 +561,8 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
553561
| CAPABILITY_ASM_HEADER_FINGERPRINT
554562
| CAPABILITY_ASM_TRACE_TAGGING_RULES
555563
| CAPABILITY_ASM_EXTENDED_DATA_COLLECTION)
564+
1 * poller.removeListeners(Product.ASM_SCA)
565+
1 * poller.removeCapabilities({ it & datadog.remoteconfig.Capabilities.CAPABILITY_ASM_SCA_VULNERABILITY_DETECTION })
556566
4 * poller.removeListeners(_)
557567
1 * poller.removeConfigurationEndListener(_)
558568
1 * poller.stop()
@@ -776,6 +786,35 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
776786
noExceptionThrown()
777787
}
778788

789+
void 'subscribes to ASM_SCA product when configuration poller is active'() {
790+
setup:
791+
appSecConfigService.init()
792+
AppSecSystem.active = false
793+
config.getAppSecActivation() >> ProductActivation.ENABLED_INACTIVE
794+
795+
when:
796+
appSecConfigService.maybeSubscribeConfigPolling()
797+
798+
then:
799+
1 * poller.addListener(Product.ASM_SCA, AppSecSCAConfigDeserializer.INSTANCE, _)
800+
1 * poller.addCapabilities({ it & datadog.remoteconfig.Capabilities.CAPABILITY_ASM_SCA_VULNERABILITY_DETECTION })
801+
}
802+
803+
void 'unsubscribes from ASM_SCA product on close'() {
804+
setup:
805+
appSecConfigService.init()
806+
AppSecSystem.active = false
807+
config.getAppSecActivation() >> ProductActivation.ENABLED_INACTIVE
808+
appSecConfigService.maybeSubscribeConfigPolling()
809+
810+
when:
811+
appSecConfigService.close()
812+
813+
then:
814+
1 * poller.removeListeners(Product.ASM_SCA)
815+
1 * poller.removeCapabilities({ it & datadog.remoteconfig.Capabilities.CAPABILITY_ASM_SCA_VULNERABILITY_DETECTION })
816+
}
817+
779818

780819
private static AppSecFeatures autoUserInstrum(String mode) {
781820
return new AppSecFeatures().tap { features ->
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package com.datadog.appsec.config
2+
3+
import spock.lang.Specification
4+
5+
class AppSecSCAConfigDeserializerTest extends Specification {
6+
7+
def "deserializes valid JSON byte array"() {
8+
given:
9+
def json = '''
10+
{
11+
"enabled": true,
12+
"instrumentation_targets": [
13+
{
14+
"class_name": "org/springframework/web/client/RestTemplate",
15+
"method_name": "execute"
16+
}
17+
]
18+
}
19+
'''
20+
def bytes = json.bytes
21+
22+
when:
23+
def config = AppSecSCAConfigDeserializer.INSTANCE.deserialize(bytes)
24+
25+
then:
26+
config != null
27+
config.enabled == true
28+
config.instrumentationTargets.size() == 1
29+
config.instrumentationTargets[0].className == "org/springframework/web/client/RestTemplate"
30+
config.instrumentationTargets[0].methodName == "execute"
31+
}
32+
33+
def "returns null for null content"() {
34+
when:
35+
def config = AppSecSCAConfigDeserializer.INSTANCE.deserialize(null)
36+
37+
then:
38+
config == null
39+
}
40+
41+
def "returns null for empty byte array"() {
42+
when:
43+
def config = AppSecSCAConfigDeserializer.INSTANCE.deserialize(new byte[0])
44+
45+
then:
46+
config == null
47+
}
48+
49+
def "deserializes minimal configuration"() {
50+
given:
51+
def json = '{"enabled": false}'
52+
def bytes = json.bytes
53+
54+
when:
55+
def config = AppSecSCAConfigDeserializer.INSTANCE.deserialize(bytes)
56+
57+
then:
58+
config != null
59+
config.enabled == false
60+
config.instrumentationTargets == null
61+
}
62+
63+
def "handles multiple instrumentation targets"() {
64+
given:
65+
def json = '''
66+
{
67+
"enabled": true,
68+
"instrumentation_targets": [
69+
{
70+
"class_name": "com/example/Class1",
71+
"method_name": "method1"
72+
},
73+
{
74+
"class_name": "com/example/Class2",
75+
"method_name": "method2"
76+
},
77+
{
78+
"class_name": "com/example/Class3",
79+
"method_name": "method3"
80+
}
81+
]
82+
}
83+
'''
84+
def bytes = json.bytes
85+
86+
when:
87+
def config = AppSecSCAConfigDeserializer.INSTANCE.deserialize(bytes)
88+
89+
then:
90+
config != null
91+
config.enabled == true
92+
config.instrumentationTargets.size() == 3
93+
94+
config.instrumentationTargets[0].className == "com/example/Class1"
95+
config.instrumentationTargets[0].methodName == "method1"
96+
97+
config.instrumentationTargets[1].className == "com/example/Class2"
98+
config.instrumentationTargets[1].methodName == "method2"
99+
100+
config.instrumentationTargets[2].className == "com/example/Class3"
101+
config.instrumentationTargets[2].methodName == "method3"
102+
}
103+
104+
def "INSTANCE is a singleton"() {
105+
expect:
106+
AppSecSCAConfigDeserializer.INSTANCE === AppSecSCAConfigDeserializer.INSTANCE
107+
}
108+
}

0 commit comments

Comments
 (0)