Skip to content

Commit

Permalink
Appsec: create top span for process executions
Browse files Browse the repository at this point in the history
  • Loading branch information
cataphract committed Oct 4, 2022
1 parent a1e870a commit 0694c11
Show file tree
Hide file tree
Showing 16 changed files with 493 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import datadog.trace.api.ProductActivationConfig;
import datadog.trace.api.gateway.SubscriptionService;
import datadog.trace.api.time.SystemTimeSource;
import datadog.trace.bootstrap.instrumentation.api.EnabledSubsystems;
import datadog.trace.util.Strings;
import java.util.Collections;
import java.util.HashMap;
Expand All @@ -26,8 +27,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<String, String> STARTED_MODULES_INFO = new HashMap<>();
Expand All @@ -53,8 +52,6 @@ private static void doStart(SubscriptionService gw, SharedCommunicationObjects s
}
log.info("AppSec is starting ({})", appSecEnabledConfig);

ACTIVE = appSecEnabledConfig == ProductActivationConfig.FULLY_ENABLED;

sco.createRemaining(config);
ConfigurationPoller configurationPoller = (ConfigurationPoller) sco.configurationPoller(config);
// may throw and abort startup
Expand Down Expand Up @@ -82,6 +79,14 @@ private static void doStart(SubscriptionService gw, SharedCommunicationObjects s
log.info("AppSec has started with {}", startedAppSecModules);
}

public static boolean isActive() {
return EnabledSubsystems.APPSEC;
}

public static void setActive(boolean status) {
EnabledSubsystems.APPSEC = status;
}

private static RateLimiter getRateLimiter(Config config, Monitoring monitoring) {
RateLimiter rateLimiter = null;
int appSecTraceRateLimit = config.getAppSecTraceRateLimit();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,18 +60,18 @@ private void subscribeConfigurationPoller() {
this.lastConfig.set(configMap);
log.info(
"New AppSec configuration has been applied. AppSec status: {}",
AppSecSystem.ACTIVE ? "active" : "inactive");
AppSecSystem.isActive() ? "active" : "inactive");
return true;
});

this.configurationPoller.addFeaturesListener(
"asm",
AppSecFeaturesDeserializer.INSTANCE,
(product, newConfig, hinter) -> {
if (AppSecSystem.ACTIVE != newConfig.enabled) {
if (AppSecSystem.isActive() != newConfig.enabled) {
log.warn("AppSec {} (runtime)", newConfig.enabled ? "enabled" : "disabled");
}
AppSecSystem.ACTIVE = newConfig.enabled;
AppSecSystem.setActive(newConfig.enabled);
return true;
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ public void init() {
subscriptionService.registerCallback(
events.requestStarted(),
() -> {
if (!AppSecSystem.ACTIVE) {
if (!AppSecSystem.isActive()) {
return RequestContextSupplier.EMPTY;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {
def initialWafConfig

when:
AppSecSystem.ACTIVE = false
AppSecSystem.active = false
appSecConfigService.init()
appSecConfigService.maybeInitPoller()
initialWafConfig = appSecConfigService.addSubConfigListener("waf", subconfigListener)
Expand Down Expand Up @@ -118,18 +118,18 @@ class AppSecConfigServiceImplSpecification extends DDSpecification {

then:
1 * subconfigListener.onNewSubconfig(AppSecConfig.valueOf([version: '2.0']))
AppSecSystem.ACTIVE == true
AppSecSystem.active == true

when:
savedFeaturesListener.accept('config_key',
savedFeaturesDeserializer.deserialize('{"enabled": false}'.bytes),
ConfigurationChangesListener.PollingRateHinter.NOOP)

then:
AppSecSystem.ACTIVE == false
AppSecSystem.active == false

cleanup:
AppSecSystem.ACTIVE = true
AppSecSystem.active = true
}

void 'error in one listener does not prevent others from running'() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ class GatewayBridgeSpecification extends DDSpecification {

void 'request_start returns null context if appsec is disabled'() {
setup:
AppSecSystem.ACTIVE = false
AppSecSystem.active = false

when:
Flow<AppSecRequestContext> startFlow = requestStartedCB.get()
Expand All @@ -104,7 +104,7 @@ class GatewayBridgeSpecification extends DDSpecification {
0 * _._

cleanup:
AppSecSystem.ACTIVE = true
AppSecSystem.active = true
}

void 'request_end closes context reports attacks and publishes event'() {
Expand Down
11 changes: 11 additions & 0 deletions dd-java-agent/instrumentation/java-lang/java-lang.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
ext {
minJavaVersionForTests = JavaVersion.VERSION_1_8
}

muzzle {
pass {
coreJdk()
}
}

apply from: "$rootDir/gradle/java.gradle"
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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 String[] helperClassNames() {
return new String[] {packageName + ".ProcessImplInstrumentationHelpers"};
}

@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");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
package datadog.trace.instrumentation.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;

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<String> 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<String, String> createTags(String[] origCommand) {
String[] command = redact(origCommand);
Map<String, String> 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<Process> future;
try {
future = (CompletableFuture<Process>) 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);
}
}
Loading

0 comments on commit 0694c11

Please sign in to comment.