diff --git a/dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie b/dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie index d10c29deab01..439234a93ea4 100644 --- a/dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie +++ b/dd-java-agent/agent-tooling/src/main/resources/datadog/trace/agent/tooling/bytebuddy/matcher/ignored_class_name.trie @@ -44,6 +44,8 @@ 0 java.security.MessageDigest # allow exception profiling instrumentation 0 java.lang.Throwable +# allow ProcessImpl instrumentation +0 java.lang.ProcessImpl 0 java.net.HttpURLConnection 0 java.net.URL 0 java.nio.DirectByteBuffer 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 4f0caefef803..ab4f4b30f4d3 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 @@ -16,6 +16,7 @@ import datadog.trace.api.ProductActivationConfig; import datadog.trace.api.gateway.SubscriptionService; import datadog.trace.api.time.SystemTimeSource; +import datadog.trace.bootstrap.ActiveSubsystems; import datadog.trace.util.Strings; import java.util.Collections; import java.util.HashMap; @@ -29,8 +30,6 @@ public class AppSecSystem { - public static volatile boolean ACTIVE; - private static final Logger log = LoggerFactory.getLogger(AppSecSystem.class); private static final AtomicBoolean STARTED = new AtomicBoolean(); private static final Map STARTED_MODULES_INFO = new HashMap<>(); @@ -44,6 +43,7 @@ public static void start(SubscriptionService gw, SharedCommunicationObjects sco) throw ase; } catch (RuntimeException | Error e) { StandardizedLogging.appSecStartupError(log, e); + setActive(false); throw new AbortStartupException(e); } } @@ -57,7 +57,6 @@ private static void doStart(SubscriptionService gw, SharedCommunicationObjects s } log.info("AppSec is starting ({})", appSecEnabledConfig); - ACTIVE = appSecEnabledConfig == ProductActivationConfig.FULLY_ENABLED; REPLACEABLE_EVENT_PRODUCER = new ReplaceableEventProducerService(); EventDispatcher eventDispatcher = new EventDispatcher(); REPLACEABLE_EVENT_PRODUCER.replaceEventProducerService(eventDispatcher); @@ -82,6 +81,8 @@ private static void doStart(SubscriptionService gw, SharedCommunicationObjects s loadModules(eventDispatcher); gatewayBridge.init(); + setActive(appSecEnabledConfig == ProductActivationConfig.FULLY_ENABLED); + APP_SEC_CONFIG_SERVICE.maybeSubscribeConfigPolling(); STARTED.set(true); @@ -102,6 +103,14 @@ private static RateLimiter getRateLimiter(Config config, Monitoring monitoring) return rateLimiter; } + public static boolean isActive() { + return ActiveSubsystems.APPSEC_ACTIVE; + } + + public static void setActive(boolean status) { + ActiveSubsystems.APPSEC_ACTIVE = status; + } + public static void stop() { if (!STARTED.getAndSet(false)) { return; diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java index e1056cb41511..35822489c271 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/config/AppSecConfigServiceImpl.java @@ -68,7 +68,7 @@ private void subscribeConfigurationPoller() { distributeSubConfigurations(configMap, reconfiguration); log.info( "New AppSec configuration has been applied. AppSec status: {}", - AppSecSystem.ACTIVE ? "active" : "inactive"); + AppSecSystem.isActive() ? "active" : "inactive"); }); this.configurationPoller.addListener( Product.ASM_DATA, @@ -107,9 +107,9 @@ private void subscribeConfigurationPoller() { (configKey, newConfig, hinter) -> { final boolean newState = newConfig != null && newConfig.asm != null && newConfig.asm.enabled; - if (AppSecSystem.ACTIVE != newState) { + if (AppSecSystem.isActive() != newState) { log.warn("AppSec {} (runtime)", newState ? "enabled" : "disabled"); - AppSecSystem.ACTIVE = newState; + AppSecSystem.setActive(newState); } }); diff --git a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java index 430a5ca257f4..352a845515eb 100644 --- a/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java +++ b/dd-java-agent/appsec/src/main/java/com/datadog/appsec/gateway/GatewayBridge.java @@ -85,7 +85,7 @@ public void init() { subscriptionService.registerCallback( events.requestStarted(), () -> { - if (!AppSecSystem.ACTIVE) { + if (!AppSecSystem.isActive()) { return RequestContextSupplier.EMPTY; } diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy index add3d4ab7f64..8ca3fd242e78 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/config/AppSecConfigServiceImplSpecification.groovy @@ -102,7 +102,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { def initialRulesOverride when: - AppSecSystem.ACTIVE = false + AppSecSystem.active = false appSecConfigService.init() appSecConfigService.maybeSubscribeConfigPolling() def configurer = appSecConfigService.createAppSecModuleConfigurer() @@ -160,7 +160,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { 1 * wafDataListener.onNewSubconfig([[id: 'foo', type: '', data: []]], _) 1 * wafRulesOverrideListener.onNewSubconfig([foo: false], _) 0 * _._ - AppSecSystem.ACTIVE == true + AppSecSystem.active == true when: savedFeaturesListener.accept('config_key', @@ -168,7 +168,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { ConfigurationChangesListener.PollingRateHinter.NOOP) then: - AppSecSystem.ACTIVE == false + AppSecSystem.active == false when: 'switch back to enabled' savedFeaturesListener.accept('config_key', @@ -176,7 +176,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { ConfigurationChangesListener.PollingRateHinter.NOOP) then: 'it is enabled again' - AppSecSystem.ACTIVE == true + AppSecSystem.active == true when: 'asm are not set' savedFeaturesListener.accept('config_key', @@ -184,7 +184,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { ConfigurationChangesListener.PollingRateHinter.NOOP) then: 'it is disabled ( == false)' - AppSecSystem.ACTIVE == false + AppSecSystem.active == false when: 'switch back to enabled' savedFeaturesListener.accept('config_key', @@ -192,7 +192,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { ConfigurationChangesListener.PollingRateHinter.NOOP) then: 'it is enabled again' - AppSecSystem.ACTIVE == true + AppSecSystem.active == true when: 'asm features are not set' savedFeaturesListener.accept('config_key', @@ -200,10 +200,10 @@ class AppSecConfigServiceImplSpecification extends DDSpecification { ConfigurationChangesListener.PollingRateHinter.NOOP) then: 'it is disabled ( == false)' - AppSecSystem.ACTIVE == false + AppSecSystem.active == false cleanup: - AppSecSystem.ACTIVE = true + AppSecSystem.active = true } void 'stopping appsec unsubscribes from the poller'() { diff --git a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy index 10136be30397..2f50cbbd7515 100644 --- a/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy +++ b/dd-java-agent/appsec/src/test/groovy/com/datadog/appsec/gateway/GatewayBridgeSpecification.groovy @@ -94,7 +94,7 @@ class GatewayBridgeSpecification extends DDSpecification { void 'request_start returns null context if appsec is disabled'() { setup: - AppSecSystem.ACTIVE = false + AppSecSystem.active = false when: Flow startFlow = requestStartedCB.get() @@ -105,7 +105,7 @@ class GatewayBridgeSpecification extends DDSpecification { 0 * _._ cleanup: - AppSecSystem.ACTIVE = true + AppSecSystem.active = true } void 'request_end closes context reports attacks and publishes event'() { diff --git a/dd-java-agent/instrumentation/java-lang/java-lang.gradle b/dd-java-agent/instrumentation/java-lang/java-lang.gradle new file mode 100644 index 000000000000..b4a2549e6663 --- /dev/null +++ b/dd-java-agent/instrumentation/java-lang/java-lang.gradle @@ -0,0 +1,11 @@ +ext { + minJavaVersionForTests = JavaVersion.VERSION_1_8 +} + +muzzle { + pass { + coreJdk() + } +} + +apply from: "$rootDir/gradle/java.gradle" diff --git a/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/ProcessImplInstrumentation.java b/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/ProcessImplInstrumentation.java new file mode 100644 index 000000000000..dfbba5e09994 --- /dev/null +++ b/dd-java-agent/instrumentation/java-lang/src/main/java/datadog/trace/instrumentation/java/lang/ProcessImplInstrumentation.java @@ -0,0 +1,42 @@ +package datadog.trace.instrumentation.java.lang; + +import static datadog.trace.agent.tooling.bytebuddy.matcher.NameMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.api.Platform; +import java.util.Map; + +@AutoService(Instrumenter.class) +public class ProcessImplInstrumentation extends Instrumenter.AppSec + implements Instrumenter.ForSingleType, Instrumenter.ForBootstrap { + + public ProcessImplInstrumentation() { + super("java-lang-appsec"); + } + + @Override + public String instrumentedType() { + return "java.lang.ProcessImpl"; + } + + @Override + public boolean isEnabled() { + return Platform.isJavaVersionAtLeast(8) && super.isEnabled(); + } + + @Override + public void adviceTransformations(AdviceTransformation transformation) { + transformation.applyAdvice( + named("start") + .and( + takesArguments( + String[].class, + Map.class, + String.class, + ProcessBuilder.Redirect[].class, + boolean.class)), + packageName + ".ProcessImplStartAdvice"); + } +} diff --git a/dd-java-agent/instrumentation/java-lang/src/main/java8/datadog/trace/instrumentation/java/lang/ProcessImplStartAdvice.java b/dd-java-agent/instrumentation/java-lang/src/main/java8/datadog/trace/instrumentation/java/lang/ProcessImplStartAdvice.java new file mode 100644 index 000000000000..8e886bccb994 --- /dev/null +++ b/dd-java-agent/instrumentation/java-lang/src/main/java8/datadog/trace/instrumentation/java/lang/ProcessImplStartAdvice.java @@ -0,0 +1,50 @@ +package datadog.trace.instrumentation.java.lang; + +import datadog.trace.bootstrap.ActiveSubsystems; +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import datadog.trace.bootstrap.instrumentation.api.AgentTracer; +import datadog.trace.bootstrap.instrumentation.api.TagContext; +import datadog.trace.bootstrap.instrumentation.api8.java.lang.ProcessImplInstrumentationHelpers; +import java.io.IOException; +import java.util.Map; +import net.bytebuddy.asm.Advice; + +class ProcessImplStartAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentSpan startSpan(@Advice.Argument(0) final String[] command) throws IOException { + if (!ProcessImplInstrumentationHelpers.ONLINE) { + return null; + } + + AgentTracer.TracerAPI tracer = AgentTracer.get(); + + if (!ActiveSubsystems.APPSEC_ACTIVE) { + return null; + } + + Map tags = ProcessImplInstrumentationHelpers.createTags(command); + TagContext tagContext = new TagContext("appsec", tags); + AgentSpan span = tracer.startSpan("command_execution", tagContext, true); + span.setSpanType("system"); + span.setResourceName(ProcessImplInstrumentationHelpers.determineResource(command)); + span.startThreadMigration(); + return span; + } + + @Advice.OnMethodExit(suppress = Throwable.class, onThrowable = Throwable.class) + public static void endSpan( + @Advice.Return Process p, @Advice.Enter AgentSpan span, @Advice.Thrown Throwable t) { + if (span == null) { + return; + } + if (t != null) { + span.finishThreadMigration(); + span.setError(true); + span.setErrorMessage(t.getMessage()); + span.finish(); + return; + } + + ProcessImplInstrumentationHelpers.addProcessCompletionHook(p, span); + } +} diff --git a/dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/ProcessImplInstrumentationSpecification.groovy b/dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/ProcessImplInstrumentationSpecification.groovy new file mode 100644 index 000000000000..809ac827085f --- /dev/null +++ b/dd-java-agent/instrumentation/java-lang/src/test/groovy/datadog/trace/instrumentation/java/lang/ProcessImplInstrumentationSpecification.groovy @@ -0,0 +1,165 @@ +package datadog.trace.instrumentation.java.lang + +import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.api.gateway.RequestContextSlot +import datadog.trace.bootstrap.ActiveSubsystems + +import datadog.trace.core.DDSpan +import spock.lang.Requires + +import java.util.concurrent.TimeUnit + +import static datadog.trace.api.Platform.isJavaVersionAtLeast + +@Requires({ + isJavaVersionAtLeast(8) +}) +class ProcessImplInstrumentationSpecification extends AgentTestRunner { + def ss = TEST_TRACER.getSubscriptionService(RequestContextSlot.APPSEC) + + void cleanup() { + ss.reset() + } + + void 'creates a span in a normal case'() { + when: + def builder = new ProcessBuilder('/bin/sh', '-c', 'echo 42') + Process p = builder.start() + String output = p.inputStream.text + TEST_WRITER.waitForTraces(1) + DDSpan span = TEST_WRITER[0][0] + + then: + output == "42\n" + span.tags['cmd.exec'] == '["/bin/sh","-c","echo 42"]' + span.tags['cmd.exit_code'] == 0 + span.spanType == 'system' + span.resourceName == 'sh' + span.spanName == 'command_execution' + } + + void 'no span is created if appsec is disabled'() { + setup: + ActiveSubsystems.APPSEC_ACTIVE = false + + when: + def builder = new ProcessBuilder('/bin/sh', '-c', 'echo 42') + Process p = builder.start() + Thread.start { p.inputStream.text } + p.waitFor(5, TimeUnit.SECONDS) + + then: + TEST_WRITER.size() == 0 + } + + void 'variant with Runtime exec'() { + when: + Process p = Runtime.runtime.exec('/bin/sh -c true') + p.waitFor(5, TimeUnit.SECONDS) + TEST_WRITER.waitForTraces(1) + DDSpan span = TEST_WRITER[0][0] + + then: + span.tags['cmd.exec'] == '["/bin/sh","-c","true"]' + } + + void 'the exit code is correctly reported'() { + when: + def builder = new ProcessBuilder('/bin/sh', '-c', 'exit 33') + Process p = builder.start() + p.waitFor(5, TimeUnit.SECONDS) + TEST_WRITER.waitForTraces(1) + DDSpan span = TEST_WRITER[0][0] + + then: + span.tags['cmd.exit_code'] == 33 + } + + void 'can handle waiting on another thread'() { + when: + // sleep a bit so that it doesn't all happen on the same thread + def builder = new ProcessBuilder('/bin/sh', '-c', 'sleep 0.5; echo 42') + Process p = builder.start() + def out + Thread.start { + out = p.inputStream.text + p.waitFor() + }.join(5000) + TEST_WRITER.waitForTraces(1) + DDSpan span = TEST_WRITER[0][0] + + then: + out == '42\n' + span.getDurationNano() >= 500_000_000 // 500 ms (we sleep for 0.5 s) + span.tags['cmd.exit_code'] == 0 + } + + void 'command cannot be executed'() { + when: + def builder = new ProcessBuilder('/bin/does-not-exist') + builder.start() + + then: + thrown IOException + + when: + TEST_WRITER.waitForTraces(1) + DDSpan span = TEST_WRITER[0][0] + + then: + span.tags['cmd.exec'] == '["/bin/does-not-exist"]' + span.tags['error.msg'] != null + span.isError() == true + } + + void 'process is destroyed'() { + when: + def builder = new ProcessBuilder('/bin/sh', '-c', 'sleep 3600') + Process p = builder.start() + Thread.start { + p.destroy() + } + p.waitFor(5, TimeUnit.SECONDS) + TEST_WRITER.waitForTraces(1) + DDSpan span = TEST_WRITER[0][0] + + then: + span.tags['cmd.exit_code'] != 0 + } + + void 'command is truncated'() { + when: + def builder = new ProcessBuilder('/bin/sh', '-c', 'echo ' + ('a' * (4096 - 14 + 1))) + Process p = builder.start() + Thread.start { p.inputStream.text } + p.waitFor(5, TimeUnit.SECONDS) + TEST_WRITER.waitForTraces(1) + DDSpan span = TEST_WRITER[0][0] + + then: + span.tags['cmd.truncated'] == 'true' + span.tags['cmd.exec'] == '["/bin/sh","-c"]' + } + + void redactions() { + when: + def builder = new ProcessBuilder(command) + builder.environment()['PATH'] = '/' + builder.start() + + then: + thrown IOException + + when: + TEST_WRITER.waitForTraces(1) + DDSpan span = TEST_WRITER[0][0] + + then: + span.tags['cmd.exec'] == expected + + where: + command | expected + ['cmd', '--pass', 'abc', '--token=def'] | '["cmd","--pass","?","--token=?"]' + ['md5', '-s', 'pony'] | '["md5","?","?"]' + } +} diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/AgentTestRunner.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/AgentTestRunner.groovy index a7cad61d661b..817d01889b07 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/AgentTestRunner.groovy +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/AgentTestRunner.groovy @@ -18,11 +18,13 @@ import datadog.trace.api.Checkpointer import datadog.trace.api.Config import datadog.trace.api.DDId import datadog.trace.api.Platform +import datadog.trace.api.ProductActivationConfig import datadog.trace.api.StatsDClient import datadog.trace.api.WellKnownTags import datadog.trace.api.config.TracerConfig import datadog.trace.api.time.SystemTimeSource import datadog.trace.api.time.TimeSource +import datadog.trace.bootstrap.ActiveSubsystems import datadog.trace.bootstrap.instrumentation.api.AgentSpan import datadog.trace.bootstrap.instrumentation.api.AgentTracer.TracerAPI import datadog.trace.common.metrics.EventListener @@ -148,6 +150,8 @@ abstract class AgentTestRunner extends DDSpecification implements AgentBuilder.L @Shared boolean isLatestDepTest = Boolean.getBoolean('test.dd.latestDepTest') + boolean originalAppSecRuntimeValue + protected boolean isDataStreamsEnabled() { return false } @@ -259,7 +263,8 @@ abstract class AgentTestRunner extends DDSpecification implements AgentBuilder.L ClassLoader classLoader, JavaModule module, ProtectionDomain pd) { - builder.method(named("isAppSecEnabled")).intercept(FixedValue.value(true)) + builder.method(named("getAppSecEnabledConfig")) + .intercept(FixedValue.value(ProductActivationConfig.FULLY_ENABLED)) } }).installOn(INSTRUMENTATION) } @@ -269,7 +274,7 @@ abstract class AgentTestRunner extends DDSpecification implements AgentBuilder.L injectSysConfig(TracerConfig.SCOPE_ITERATION_KEEP_ALIVE, "1") // don't let iteration spans linger } - def setup() { + void setup() { // re-register in case a new JVM is spawned TimelineTracingContextTracker.register() @@ -296,6 +301,9 @@ abstract class AgentTestRunner extends DDSpecification implements AgentBuilder.L util.attachMock(STATS_D_CLIENT, this) util.attachMock(TEST_CHECKPOINTER, this) util.attachMock(TEST_TRACKER, this) + + originalAppSecRuntimeValue = ActiveSubsystems.APPSEC_ACTIVE + ActiveSubsystems.APPSEC_ACTIVE = true } void cleanup() { @@ -308,12 +316,14 @@ abstract class AgentTestRunner extends DDSpecification implements AgentBuilder.L TEST_TRACKER.print() TEST_CHECKPOINTER.throwOnInvalidSequence(TEST_SPANS) + + ActiveSubsystems.APPSEC_ACTIVE = originalAppSecRuntimeValue } /** Override to clean up things after the agent is removed */ protected void cleanupAfterAgent() {} - def cleanupSpec() { + void cleanupSpec() { TEST_TRACER?.close() if (null != activeTransformer) { diff --git a/gradle/forbiddenApiFilters/main.txt b/gradle/forbiddenApiFilters/main.txt index 6548a09ef0b9..fef1029cb0d5 100644 --- a/gradle/forbiddenApiFilters/main.txt +++ b/gradle/forbiddenApiFilters/main.txt @@ -2,7 +2,6 @@ java.lang.Class#forName(java.lang.String) # String methods which uses regexes for matching -java.lang.String#replace(java.lang.CharSequence,java.lang.CharSequence) java.lang.String#split(java.lang.String) java.lang.String#split(java.lang.String,int) java.lang.String#replaceAll(java.lang.String,java.lang.String) diff --git a/internal-api/internal-api-8/internal-api-8.gradle b/internal-api/internal-api-8/internal-api-8.gradle index e4faaf8826dc..71521cf06fbf 100644 --- a/internal-api/internal-api-8/internal-api-8.gradle +++ b/internal-api/internal-api-8/internal-api-8.gradle @@ -23,7 +23,10 @@ targetCompatibility = JavaVersion.VERSION_1_8 minimumBranchCoverage = 0.8 minimumInstructionCoverage = 0.8 -excludedClassesCoverage += ["datadog.trace.api.sampling.ConstantSampler"] +excludedClassesCoverage += [ + 'datadog.trace.api.sampling.ConstantSampler', + 'datadog.trace.bootstrap.instrumentation.java.lang.ProcessImplInstrumentationHelpers', +] excludedClassesBranchCoverage = [ 'datadog.trace.util.stacktrace.HotSpotStackWalker', diff --git a/internal-api/internal-api-8/src/main/java/datadog/trace/bootstrap/instrumentation/api8/java/lang/ProcessImplInstrumentationHelpers.java b/internal-api/internal-api-8/src/main/java/datadog/trace/bootstrap/instrumentation/api8/java/lang/ProcessImplInstrumentationHelpers.java new file mode 100644 index 000000000000..14a9272534eb --- /dev/null +++ b/internal-api/internal-api-8/src/main/java/datadog/trace/bootstrap/instrumentation/api8/java/lang/ProcessImplInstrumentationHelpers.java @@ -0,0 +1,183 @@ +package datadog.trace.bootstrap.instrumentation.api8.java.lang; + +import static java.lang.invoke.MethodType.methodType; + +import datadog.trace.bootstrap.instrumentation.api.AgentSpan; +import java.lang.invoke.MethodHandle; +import java.lang.invoke.MethodHandles; +import java.lang.reflect.Field; +import java.lang.reflect.UndeclaredThrowableException; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; +import java.util.regex.Pattern; + +/** This class is included here because it needs to injected into the bootstrap clasloader. */ +public class ProcessImplInstrumentationHelpers { + private static final int LIMIT = 4096; + public static final boolean ONLINE; + private static final MethodHandle PROCESS_ON_EXIT; + private static final Executor EXECUTOR; + + private static final Pattern REDACTED_PARAM_PAT = + Pattern.compile( + "^(?i)-{0,2}(?:p(?:ass(?:w(?:or)?d)?)?|api_?key|secret|" + + "a(?:ccess|uth)_token|mysql_pwd|credentials|(?:stripe)?token)$"); + private static final Set REDACTED_BINARIES = Collections.singleton("md5"); + + static { + MethodHandle processOnExit = null; + Executor executor = null; + try { + // java 9 + processOnExit = + MethodHandles.publicLookup() + .findVirtual(Process.class, "onExit", methodType(CompletableFuture.class)); + } catch (Throwable e) { + try { + // java 8 + Class unixProcessCls = + ClassLoader.getSystemClassLoader().loadClass("java.lang.UNIXProcess"); + Field f = unixProcessCls.getDeclaredField("processReaperExecutor"); + f.setAccessible(true); + executor = (Executor) f.get(null); + } catch (ClassNotFoundException | NoSuchFieldException | IllegalAccessException ex) { + } + } + PROCESS_ON_EXIT = processOnExit; + EXECUTOR = executor; + ONLINE = PROCESS_ON_EXIT != null || EXECUTOR != null; + } + + private ProcessImplInstrumentationHelpers() {} + + public static Map createTags(String[] origCommand) { + String[] command = redact(origCommand); + Map ret = new HashMap<>(4); + StringBuilder sb = new StringBuilder("["); + long remaining = LIMIT; + for (int i = 0; i < command.length; i++) { + String cur = command[i]; + remaining -= cur.length(); + if (remaining < 0) { + ret.put("cmd.truncated", "true"); + break; + } + if (i != 0) { + sb.append(','); + } + sb.append('"'); + sb.append(cur.replace("\\", "\\\\").replace("\"", "\\\"")); + sb.append('"'); + } + sb.append("]"); + ret.put("cmd.exec", sb.toString()); + return ret; + } + + private static String[] redact(String[] command) { + if (command.length == 0) { + return command; + } + + String first = command[0]; + String[] newCommand = null; + if (REDACTED_BINARIES.contains(first)) { + newCommand = new String[command.length]; + newCommand[0] = first; + for (int i = 1; i < command.length; i++) { + newCommand[i] = "?"; + } + return newCommand; + } + + boolean redactNext = false; + for (int i = 1; i < command.length; i++) { + if (redactNext) { + if (newCommand == null) { + newCommand = new String[command.length]; + System.arraycopy(command, 0, newCommand, 0, command.length); + } + newCommand[i] = "?"; + redactNext = false; + continue; + } + + String s = command[i]; + if (s == null) { + continue; + } + int posEqual = s.indexOf('='); + if (posEqual == -1) { + if (REDACTED_PARAM_PAT.matcher(s).matches()) { + redactNext = true; + } + } else { + String param = s.substring(0, posEqual); + if (REDACTED_PARAM_PAT.matcher(param).matches()) { + if (newCommand == null) { + newCommand = new String[command.length]; + System.arraycopy(command, 0, newCommand, 0, command.length); + } + newCommand[i] = param + "=?"; + } + } + } + + return newCommand != null ? newCommand : command; + } + + public static void addProcessCompletionHook(Process p, AgentSpan span) { + if (PROCESS_ON_EXIT != null) { + CompletableFuture future; + try { + future = (CompletableFuture) PROCESS_ON_EXIT.invokeExact(p); + } catch (Throwable e) { + if (e instanceof Error) { + throw (Error) e; + } else if (e instanceof RuntimeException) { + throw (RuntimeException) e; + } else { + throw new UndeclaredThrowableException(e); + } + } + + future.whenComplete( + (process, thr) -> { + span.finishThreadMigration(); + if (thr != null) { + span.setError(true); + span.setErrorMessage(thr.getMessage()); + } else { + span.setTag("cmd.exit_code", process.exitValue()); + } + span.finish(); + }); + } else if (EXECUTOR != null) { + EXECUTOR.execute( + () -> { + try { + int exitCode = p.waitFor(); + span.finishThreadMigration(); + span.setTag("cmd.exit_code", exitCode); + } catch (InterruptedException e) { + span.setError(true); + span.setErrorMessage(e.getMessage()); + } + span.finish(); + }); + } + } + + public static CharSequence determineResource(String[] command) { + String first = command[0]; + int pos = first.lastIndexOf('/'); + if (pos == -1 || pos == first.length() - 1) { + return first; + } + return first.substring(pos + 1); + } +} diff --git a/internal-api/internal-api.gradle b/internal-api/internal-api.gradle index 61a6c70c78d8..f6663b55c99d 100644 --- a/internal-api/internal-api.gradle +++ b/internal-api/internal-api.gradle @@ -21,6 +21,7 @@ excludedClassesCoverage += [ // this one is covered by tests in internal-api-8 together with AdaptiveSampler8 "datadog.trace.api.sampling.AdaptiveSampler7*", "datadog.trace.api.sampling.ConstantSampler", + "datadog.trace.bootstrap.ActiveSubsystems", "datadog.trace.bootstrap.config.provider.ConfigProvider.Singleton", "datadog.trace.bootstrap.instrumentation.api.Tags", "datadog.trace.bootstrap.instrumentation.api.CommonTagValues", diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/ActiveSubsystems.java b/internal-api/src/main/java/datadog/trace/bootstrap/ActiveSubsystems.java new file mode 100644 index 000000000000..2a43c32c3f37 --- /dev/null +++ b/internal-api/src/main/java/datadog/trace/bootstrap/ActiveSubsystems.java @@ -0,0 +1,5 @@ +package datadog.trace.bootstrap; + +public class ActiveSubsystems { + public static volatile boolean APPSEC_ACTIVE; +} diff --git a/settings.gradle b/settings.gradle index d12ddb5fa79f..e33990b162d0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -189,6 +189,7 @@ include ':dd-java-agent:instrumentation:http-url-connection' include ':dd-java-agent:instrumentation:hystrix-1.4' include ':dd-java-agent:instrumentation:ignite-2.0' include ':dd-java-agent:instrumentation:java-security' +include ':dd-java-agent:instrumentation:java-lang' include ':dd-java-agent:instrumentation:jakarta-rs-annotations-3' include ':dd-java-agent:instrumentation:java-concurrent' include ':dd-java-agent:instrumentation:java-concurrent:java-completablefuture'