Merge pull request #152 from DataDog/ark/mongo_quickfix

Fix Mongo Query Sanitizing
This commit is contained in:
Ark 2017-11-13 10:36:21 -05:00 committed by GitHub
commit ce40cbfbed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 163 additions and 85 deletions

View File

@ -19,7 +19,7 @@ public class MongoClientInstrumentationTest {
.get(0)
.getClass()
.getSimpleName())
.isEqualTo("TracingCommandListener");
.isEqualTo("DDTracingCommandListener");
mongoClient.close();
}

View File

@ -14,7 +14,6 @@ dependencies {
compileOnly group: 'org.jboss.byteman', name: 'byteman', version: '4.0.0-BETA5'
compile group: 'io.opentracing.contrib', name: 'opentracing-web-servlet-filter', version: '0.0.9'
compile group: 'io.opentracing.contrib', name: 'opentracing-mongo-driver', version: '0.0.3'
compile group: 'io.opentracing.contrib', name: 'opentracing-okhttp3', version: '0.0.5'
compile group: 'io.opentracing.contrib', name: 'opentracing-jms-common', version: '0.0.3'
compile group: 'io.opentracing.contrib', name: 'opentracing-jms-2', version: '0.0.3'

View File

@ -2,14 +2,21 @@ package com.datadoghq.agent.integration;
import com.datadoghq.trace.DDTags;
import com.mongodb.MongoClientOptions;
import com.mongodb.event.CommandFailedEvent;
import com.mongodb.event.CommandListener;
import com.mongodb.event.CommandStartedEvent;
import com.mongodb.event.CommandSucceededEvent;
import io.opentracing.Span;
import io.opentracing.contrib.mongo.TracingCommandListener;
import io.opentracing.contrib.mongo.TracingCommandListenerFactory;
import io.opentracing.Tracer;
import io.opentracing.tag.Tags;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import lombok.extern.slf4j.Slf4j;
import org.bson.BsonArray;
import org.bson.BsonDocument;
@ -21,10 +28,6 @@ import org.jboss.byteman.rule.Rule;
@Slf4j
public class MongoHelper extends DDAgentTracingHelper<MongoClientOptions.Builder> {
private static final List<String> WHILDCARD_FIELDS =
Arrays.asList("ordered", "insert", "count", "find");
private static final BsonValue HIDDEN_CAR = new BsonString("?");
public MongoHelper(final Rule rule) {
super(rule);
}
@ -42,7 +45,7 @@ public class MongoHelper extends DDAgentTracingHelper<MongoClientOptions.Builder
protected MongoClientOptions.Builder doPatch(final MongoClientOptions.Builder builder)
throws Exception {
final TracingCommandListener listener = TracingCommandListenerFactory.create(tracer);
final DDTracingCommandListener listener = new DDTracingCommandListener(tracer);
builder.addCommandListener(listener);
setState(builder, 1);
@ -50,54 +53,129 @@ public class MongoHelper extends DDAgentTracingHelper<MongoClientOptions.Builder
return builder;
}
public void decorate(final Span span, final CommandStartedEvent event) {
try {
// normalize the Mongo command so that parameters are removed from the string
final BsonDocument normalized = norm(event.getCommand());
final String mongoCmd = normalized.toString();
public static class DDTracingCommandListener implements CommandListener {
/**
* The values of these mongo fields will not be scrubbed out. This allows the non-sensitive
* collection names to be captured.
*/
private static final List<String> UNSCRUBBED_FIELDS =
Arrays.asList("ordered", "insert", "count", "find", "create");
// add specific resource name and replace the `db.statement` OpenTracing
// tag with the quantized version of the Mongo command
span.setTag(DDTags.RESOURCE_NAME, mongoCmd);
span.setTag(Tags.DB_STATEMENT.getKey(), mongoCmd);
span.setTag(DDTags.SPAN_TYPE, "mongodb");
} catch (final Throwable e) {
log.warn("Couldn't decorate the mongo query: " + e.getMessage(), e);
private static final BsonValue HIDDEN_CHAR = new BsonString("?");
private static final String MONGO_OPERATION = "mongo.query";
static final String COMPONENT_NAME = "java-mongo";
private final Tracer tracer;
/** Cache for (request id, span) pairs */
private final Map<Integer, Span> cache = new ConcurrentHashMap<>();
public DDTracingCommandListener(Tracer tracer) {
this.tracer = tracer;
}
}
private BsonDocument norm(final BsonDocument origin) {
final BsonDocument normalized = new BsonDocument();
for (final Map.Entry<String, BsonValue> entry : origin.entrySet()) {
if (WHILDCARD_FIELDS.contains(entry.getKey())) {
normalized.put(entry.getKey(), entry.getValue());
} else {
final BsonValue child = norm(entry.getValue());
normalized.put(entry.getKey(), child);
@Override
public void commandStarted(CommandStartedEvent event) {
Span span = buildSpan(event);
cache.put(event.getRequestId(), span);
}
@Override
public void commandSucceeded(CommandSucceededEvent event) {
Span span = cache.remove(event.getRequestId());
if (span != null) {
span.finish();
}
}
return normalized;
}
private BsonValue norm(final BsonArray origin) {
final BsonArray normalized = new BsonArray();
for (final BsonValue value : origin) {
final BsonValue child = norm(value);
normalized.add(child);
@Override
public void commandFailed(CommandFailedEvent event) {
Span span = cache.remove(event.getRequestId());
if (span != null) {
onError(span, event.getThrowable());
span.finish();
}
}
return normalized;
}
private BsonValue norm(final BsonValue origin) {
private Span buildSpan(CommandStartedEvent event) {
Tracer.SpanBuilder spanBuilder =
tracer.buildSpan(MONGO_OPERATION).withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT);
final BsonValue normalized;
if (origin.isDocument()) {
normalized = norm(origin.asDocument());
} else if (origin.isArray()) {
normalized = norm(origin.asArray());
} else {
normalized = HIDDEN_CAR;
Span span = spanBuilder.startManual();
try {
decorate(span, event);
} catch (final Throwable e) {
log.warn("Couldn't decorate the mongo query: " + e.getMessage(), e);
}
return span;
}
private static void onError(Span span, Throwable throwable) {
Tags.ERROR.set(span, Boolean.TRUE);
span.log(Collections.singletonMap("error.object", throwable));
}
public static void decorate(Span span, CommandStartedEvent event) {
// scrub the Mongo command so that parameters are removed from the string
final BsonDocument scrubbed = scrub(event.getCommand());
final String mongoCmd = scrubbed.toString();
Tags.COMPONENT.set(span, COMPONENT_NAME);
Tags.DB_STATEMENT.set(span, mongoCmd);
Tags.DB_INSTANCE.set(span, event.getDatabaseName());
// add specific resource name
span.setTag(DDTags.RESOURCE_NAME, mongoCmd);
span.setTag(DDTags.SPAN_TYPE, "mongodb");
span.setTag(DDTags.SERVICE_NAME, "mongo");
Tags.PEER_HOSTNAME.set(span, event.getConnectionDescription().getServerAddress().getHost());
InetAddress inetAddress =
event.getConnectionDescription().getServerAddress().getSocketAddress().getAddress();
if (inetAddress instanceof Inet4Address) {
byte[] address = inetAddress.getAddress();
Tags.PEER_HOST_IPV4.set(span, ByteBuffer.wrap(address).getInt());
} else {
Tags.PEER_HOST_IPV6.set(span, inetAddress.getHostAddress());
}
Tags.PEER_PORT.set(span, event.getConnectionDescription().getServerAddress().getPort());
Tags.DB_TYPE.set(span, "mongo");
}
private static BsonDocument scrub(final BsonDocument origin) {
final BsonDocument scrub = new BsonDocument();
for (final Map.Entry<String, BsonValue> entry : origin.entrySet()) {
if (UNSCRUBBED_FIELDS.contains(entry.getKey()) && entry.getValue().isString()) {
scrub.put(entry.getKey(), entry.getValue());
} else {
final BsonValue child = scrub(entry.getValue());
scrub.put(entry.getKey(), child);
}
}
return scrub;
}
private static BsonValue scrub(final BsonArray origin) {
final BsonArray scrub = new BsonArray();
for (final BsonValue value : origin) {
final BsonValue child = scrub(value);
scrub.add(child);
}
return scrub;
}
private static BsonValue scrub(final BsonValue origin) {
final BsonValue scrubbed;
if (origin.isDocument()) {
scrubbed = scrub(origin.asDocument());
} else if (origin.isArray()) {
scrubbed = scrub(origin.asArray());
} else {
scrubbed = HIDDEN_CHAR;
}
return scrubbed;
}
return normalized;
}
}

View File

@ -1,14 +0,0 @@
package io.opentracing.contrib.mongo;
import io.opentracing.Tracer;
/**
* This class exists purely to bypass the reduction in constructor visibility of
* TracingCommandListener.
*/
public class TracingCommandListenerFactory {
public static TracingCommandListener create(final Tracer tracer) {
return new TracingCommandListener(tracer);
}
}

View File

@ -48,15 +48,6 @@ DO
com.datadoghq.agent.InstrumentationRulesManager.registerClassLoad($0);
ENDRULE
RULE TracingCommandListener-init
CLASS io.opentracing.contrib.mongo.TracingCommandListener
METHOD <init>
AT EXIT
IF TRUE
DO
com.datadoghq.agent.InstrumentationRulesManager.registerClassLoad($0);
ENDRULE
# Instrument OkHttp
# ===========================

View File

@ -70,16 +70,6 @@ DO
patch($0);
ENDRULE
RULE opentracing-mongo-driver-helper
CLASS io.opentracing.contrib.mongo.TracingCommandListener
METHOD decorate
HELPER com.datadoghq.agent.integration.MongoHelper
AT EXIT
IF TRUE
DO
decorate($1, $2);
ENDRULE
# Instrument OkHttp
# ===========================

View File

@ -4,23 +4,57 @@ import static org.assertj.core.api.Java6Assertions.assertThat;
import com.datadoghq.trace.DDSpan;
import com.datadoghq.trace.DDTracer;
import com.mongodb.ServerAddress;
import com.mongodb.connection.ClusterId;
import com.mongodb.connection.ConnectionDescription;
import com.mongodb.connection.ServerId;
import com.mongodb.event.CommandStartedEvent;
import io.opentracing.tag.Tags;
import java.util.Arrays;
import java.util.List;
import org.bson.BsonArray;
import org.bson.BsonDocument;
import org.bson.BsonString;
import org.junit.Test;
public class MongoHelperTest {
@Test
public void test() {
private static ConnectionDescription makeConnection() {
return new ConnectionDescription(new ServerId(new ClusterId(), new ServerAddress()));
}
@Test
public void mongoSpan() {
final CommandStartedEvent cmd =
new CommandStartedEvent(1, null, "databasename", "query", new BsonDocument());
new CommandStartedEvent(1, makeConnection(), "databasename", "query", new BsonDocument());
final DDSpan span = new DDTracer().buildSpan("foo").startManual();
new MongoHelper(null).decorate(span, cmd);
MongoHelper.DDTracingCommandListener.decorate(span, cmd);
assertThat(span.context().getSpanType()).isEqualTo("mongodb");
assertThat(span.context().getResourceName())
.isEqualTo(span.context().getTags().get("db.statement"));
}
@Test
public void queryScrubbing() {
// all "secret" strings should be scrubbed out of these queries
BsonDocument query1 = new BsonDocument("find", new BsonString("show"));
query1.put("stuff", new BsonString("secret"));
BsonDocument query2 = new BsonDocument("insert", new BsonString("table"));
BsonDocument query2_1 = new BsonDocument("count", new BsonString("show"));
query2_1.put("id", new BsonString("secret"));
query2.put("docs", new BsonArray(Arrays.asList(new BsonString("secret"), query2_1)));
List<BsonDocument> queries = Arrays.asList(query1, query2);
for (BsonDocument query : queries) {
final CommandStartedEvent cmd =
new CommandStartedEvent(1, makeConnection(), "databasename", "query", query);
final DDSpan span = new DDTracer().buildSpan("foo").startManual();
MongoHelper.DDTracingCommandListener.decorate(span, cmd);
assertThat(span.getTags().get(Tags.DB_STATEMENT.getKey()))
.isEqualTo(query.toString().replaceAll("secret", "?"));
}
}
}