diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java index 555e027d498..8d2f536d048 100644 --- a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/Agent.java @@ -1137,26 +1137,30 @@ private static void maybeStartAiGuard() { private static void maybeStartAppSec(Class scoClass, Object o) { try { - // event tracking SDK must be available for customers even if AppSec is fully disabled - AppSecEventTracker.install(); - } catch (final Exception e) { - log.debug("Error starting AppSec Event Tracker", e); - } + try { + // event tracking SDK must be available for customers even if AppSec is fully disabled + AppSecEventTracker.install(); + } catch (final Exception e) { + log.debug("Error starting AppSec Event Tracker", e); + } - if (!(appSecEnabled || (remoteConfigEnabled && !appSecFullyDisabled))) { - return; - } + if (appSecEnabled || (remoteConfigEnabled && !appSecFullyDisabled)) { + StaticEventLogger.begin("AppSec"); - StaticEventLogger.begin("AppSec"); + try { + SubscriptionService ss = + AgentTracer.get().getSubscriptionService(RequestContextSlot.APPSEC); + startAppSec(ss, scoClass, o); + } catch (Exception e) { + log.error("Error starting AppSec System", e); + } - try { - SubscriptionService ss = AgentTracer.get().getSubscriptionService(RequestContextSlot.APPSEC); - startAppSec(ss, scoClass, o); - } catch (Exception e) { - log.error("Error starting AppSec System", e); + StaticEventLogger.end("AppSec"); + } + } finally { + // RFC-1110: report the agentic-onboarding signal even if the AppSec startup attempt errored. + AgenticOnboarding.report(); } - - StaticEventLogger.end("AppSec"); } private static void startAppSec(SubscriptionService ss, Class scoClass, Object sco) { diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgenticOnboarding.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgenticOnboarding.java new file mode 100644 index 00000000000..3bb9be3e4a7 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/bootstrap/AgenticOnboarding.java @@ -0,0 +1,91 @@ +package datadog.trace.bootstrap; + +import static datadog.trace.bootstrap.config.provider.StableConfigSource.FLEET; +import static datadog.trace.bootstrap.config.provider.StableConfigSource.LOCAL; +import static datadog.trace.util.ConfigStrings.propertyNameToSystemPropertyName; +import static datadog.trace.util.ConfigStrings.toEnvVar; + +import datadog.environment.EnvironmentVariables; +import datadog.environment.SystemProperties; +import datadog.trace.api.ConfigCollector; +import datadog.trace.api.ConfigOrigin; +import datadog.trace.api.ConfigSetting; +import datadog.trace.api.config.AppSecConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * RFC-1110: when the presence-only flag {@link AppSecConfig#APPSEC_AGENTIC_ONBOARDING} is set, + * report a boolean telemetry entry for whether AppSec fully came up (enabled and WAF started), + * tagged with the flag's origin. The flag's value is never read; unset means nothing is reported. + */ +final class AgenticOnboarding { + private static final Logger log = LoggerFactory.getLogger(AgenticOnboarding.class); + + private AgenticOnboarding() {} + + /** Report the signal for the current AppSec startup state; no-op when the flag is unset. */ + static void report() { + try { + final ConfigOrigin origin = resolveOrigin(); + report(ActiveSubsystems.APPSEC_WAF_STARTED, origin, configIdFor(origin)); + } catch (Throwable t) { + log.debug("Unable to report agentic onboarding telemetry", t); + } + } + + static void report(boolean wafStarted, ConfigOrigin origin, String configId) { + if (origin == null) { + return; + } + ConfigCollector.get() + .put( + AppSecConfig.APPSEC_AGENTIC_ONBOARDING, + wafStarted, + origin, + ConfigSetting.NON_DEFAULT_SEQ_ID, + configId); + } + + private static ConfigOrigin resolveOrigin() { + final String key = AppSecConfig.APPSEC_AGENTIC_ONBOARDING; + final String sysProp = propertyNameToSystemPropertyName(key); + return originOf( + SystemProperties.get(sysProp), + FLEET.get(key), + EnvironmentVariables.get(toEnvVar(sysProp)), + LOCAL.get(key)); + } + + /** + * Stable-config id of the matched source, so fleet/local signals stay attributable; else null. + */ + private static String configIdFor(ConfigOrigin origin) { + if (origin == ConfigOrigin.FLEET_STABLE_CONFIG) { + return FLEET.getConfigId(); + } + if (origin == ConfigOrigin.LOCAL_STABLE_CONFIG) { + return LOCAL.getConfigId(); + } + return null; + } + + /** + * Presence-only precedence jvm_prop > fleet > env > local; any non-null value counts. + */ + static ConfigOrigin originOf(String jvmProp, String fleet, String env, String local) { + if (jvmProp != null) { + return ConfigOrigin.JVM_PROP; + } + if (fleet != null) { + return ConfigOrigin.FLEET_STABLE_CONFIG; + } + if (env != null) { + return ConfigOrigin.ENV; + } + if (local != null) { + return ConfigOrigin.LOCAL_STABLE_CONFIG; + } + return null; + } +} diff --git a/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/AgenticOnboardingTest.java b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/AgenticOnboardingTest.java new file mode 100644 index 00000000000..3b331d948b7 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/test/java/datadog/trace/bootstrap/AgenticOnboardingTest.java @@ -0,0 +1,95 @@ +package datadog.trace.bootstrap; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +import datadog.trace.api.ConfigCollector; +import datadog.trace.api.ConfigOrigin; +import datadog.trace.api.ConfigSetting; +import datadog.trace.api.config.AppSecConfig; +import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class AgenticOnboardingTest { + + @BeforeEach + void drainCollector() { + // ConfigCollector is a JVM-wide singleton; drain it so each test sees only its own entries. + ConfigCollector.get().collect(); + } + + @Test + void originPresenceOnlyAndPrecedence() { + // unset in every source + assertNull(AgenticOnboarding.originOf(null, null, null, null)); + // any non-null value counts as set, including an empty string + assertEquals(ConfigOrigin.JVM_PROP, AgenticOnboarding.originOf("", null, null, null)); + assertEquals( + ConfigOrigin.FLEET_STABLE_CONFIG, AgenticOnboarding.originOf(null, "0", null, null)); + assertEquals(ConfigOrigin.ENV, AgenticOnboarding.originOf(null, null, "false", null)); + assertEquals( + ConfigOrigin.LOCAL_STABLE_CONFIG, AgenticOnboarding.originOf(null, null, null, "x")); + // precedence jvm_prop > fleet > env > local + assertEquals(ConfigOrigin.JVM_PROP, AgenticOnboarding.originOf("a", "b", "c", "d")); + assertEquals(ConfigOrigin.FLEET_STABLE_CONFIG, AgenticOnboarding.originOf(null, "b", "c", "d")); + assertEquals(ConfigOrigin.ENV, AgenticOnboarding.originOf(null, null, "c", "d")); + } + + @Test + void reportsNothingWhenFlagUnset() { + AgenticOnboarding.report(true, null, null); + assertNull(find()); + } + + @Test + void reportsBooleanWithOriginAndNoConfigIdForEnv() { + AgenticOnboarding.report(true, ConfigOrigin.ENV, null); + ConfigSetting setting = find(); + assertEquals(Boolean.TRUE, setting.value); + assertEquals(ConfigOrigin.ENV, setting.origin); + assertNull(setting.configId); + } + + @Test + void reportsFalseWithStableConfigId() { + AgenticOnboarding.report(false, ConfigOrigin.FLEET_STABLE_CONFIG, "cfg-42"); + ConfigSetting setting = find(); + assertEquals(Boolean.FALSE, setting.value); + assertEquals(ConfigOrigin.FLEET_STABLE_CONFIG, setting.origin); + assertEquals("cfg-42", setting.configId); + } + + @Test + void reportReadsSystemPropertyAndActiveState() { + final String sysProp = "dd.appsec.agentic_onboarding"; + final String previousProp = System.getProperty(sysProp); + final boolean previous = ActiveSubsystems.APPSEC_WAF_STARTED; + System.setProperty(sysProp, "anything"); + ActiveSubsystems.APPSEC_WAF_STARTED = true; + try { + AgenticOnboarding.report(); + ConfigSetting setting = find(); + assertEquals(Boolean.TRUE, setting.value); + assertEquals(ConfigOrigin.JVM_PROP, setting.origin); + } finally { + if (previousProp == null) { + System.clearProperty(sysProp); + } else { + System.setProperty(sysProp, previousProp); + } + ActiveSubsystems.APPSEC_WAF_STARTED = previous; + } + } + + /** Drains the collector and returns the reported agentic-onboarding setting, or {@code null}. */ + private static ConfigSetting find() { + for (Map byKey : ConfigCollector.get().collect().values()) { + ConfigSetting setting = byKey.get(AppSecConfig.APPSEC_AGENTIC_ONBOARDING); + if (setting != null) { + return setting; + } + } + return null; + } +} diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/AppSecSystem.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/AppSecSystem.java index 7258637b5bd..a02e824dff5 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/AppSecSystem.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/AppSecSystem.java @@ -61,6 +61,8 @@ public static void start(SubscriptionService gw, SharedCommunicationObjects sco) private static void doStart(SubscriptionService gw, SharedCommunicationObjects sco) { final Config config = Config.get(); ProductActivation appSecEnabledConfig = config.getAppSecActivation(); + // RFC-1110: cleared per start attempt; set below only once AppSec is fully up with its WAF. + ActiveSubsystems.APPSEC_WAF_STARTED = false; if (appSecEnabledConfig == ProductActivation.FULLY_DISABLED) { log.debug("AppSec: disabled"); return; @@ -103,6 +105,11 @@ private static void doStart(SubscriptionService gw, SharedCommunicationObjects s STARTED.set(true); + // RFC-1110: AppSec fully up only when active and its WAF module started (a swallowed WAF init + // failure leaves AppSec active but STARTED_MODULES_INFO empty). + ActiveSubsystems.APPSEC_WAF_STARTED = + ActiveSubsystems.APPSEC_ACTIVE && !STARTED_MODULES_INFO.isEmpty(); + String startedAppSecModules = String.join(", ", STARTED_MODULES_INFO.values()); if (appSecEnabledConfig == ProductActivation.FULLY_ENABLED) { log.info("AppSec is {} with {}", appSecEnabledConfig, startedAppSecModules); diff --git a/dd-trace-api/src/main/java/datadog/trace/api/config/AppSecConfig.java b/dd-trace-api/src/main/java/datadog/trace/api/config/AppSecConfig.java index 5c45f8901eb..a1783ee6fa7 100644 --- a/dd-trace-api/src/main/java/datadog/trace/api/config/AppSecConfig.java +++ b/dd-trace-api/src/main/java/datadog/trace/api/config/AppSecConfig.java @@ -4,6 +4,10 @@ public final class AppSecConfig { public static final String APPSEC_ENABLED = "appsec.enabled"; + + /** RFC-1110 presence-only flag; its value is never read, only reported as telemetry. */ + public static final String APPSEC_AGENTIC_ONBOARDING = "appsec.agentic_onboarding"; + public static final String APPSEC_RULES_FILE = "appsec.rules"; public static final String APPSEC_IP_ADDR_HEADER = "appsec.ipheader"; public static final String APPSEC_TRACE_RATE_LIMIT = "appsec.trace.rate.limit"; diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/ActiveSubsystems.java b/internal-api/src/main/java/datadog/trace/bootstrap/ActiveSubsystems.java index 2a43c32c3f3..ab2e6ed009b 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/ActiveSubsystems.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/ActiveSubsystems.java @@ -2,4 +2,9 @@ public class ActiveSubsystems { public static volatile boolean APPSEC_ACTIVE; + + /** + * Whether AppSec came up with its WAF started; stricter than {@link #APPSEC_ACTIVE} (RFC-1110). + */ + public static volatile boolean APPSEC_WAF_STARTED; } diff --git a/metadata/supported-configurations.json b/metadata/supported-configurations.json index 6241dc3e6af..9458917b252 100644 --- a/metadata/supported-configurations.json +++ b/metadata/supported-configurations.json @@ -241,6 +241,14 @@ "aliases": [] } ], + "DD_APPSEC_AGENTIC_ONBOARDING": [ + { + "version": "A", + "type": "string", + "default": null, + "aliases": [] + } + ], "DD_APPSEC_AUTOMATED_USER_EVENTS_TRACKING": [ { "version": "C",