Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -576,16 +576,24 @@ public Context beforeFinish(Context context) {
}

protected void finishInferredProxySpan(Context context) {
InferredProxySpan span;
if ((span = InferredProxySpan.fromContext(context)) != null) {
span.finish();
InferredProxySpan inferredProxySpan;
if ((inferredProxySpan = InferredProxySpan.fromContext(context)) != null) {
inferredProxySpan.finish(AgentSpan.fromContext(context));
}
}

private void onRequestEndForInstrumentationGateway(@Nonnull final AgentSpan span) {
if (span.getLocalRootSpan() != span) {
AgentSpan localRoot = span.getLocalRootSpan();

// Check if the local root is an inferred proxy span
boolean hasInferredProxyParent =
localRoot != span && localRoot.getTag("_dd.inferred_span") != null;

// Only proceed if this is the root span OR if we have an inferred proxy parent
if (localRoot != span && !hasInferredProxyParent) {
return;
}

CallbackProvider cbp = tracer().getUniversalCallbackProvider();
RequestContext requestContext = span.getRequestContext();
if (cbp != null && requestContext != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
import datadog.trace.api.telemetry.LoginEvent;
import datadog.trace.api.telemetry.RuleType;
import datadog.trace.api.telemetry.WafMetricCollector;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.bootstrap.instrumentation.api.Tags;
import datadog.trace.bootstrap.instrumentation.api.URIDataAdapter;
import datadog.trace.util.stacktrace.StackTraceEvent;
Expand Down Expand Up @@ -847,6 +848,7 @@ private NoopFlow onRequestEnded(RequestContext ctx_, IGSpanInfo spanInfo) {
}
ctx.setRequestEndCalled();

AgentSpan span = (AgentSpan) spanInfo;
TraceSegment traceSeg = ctx_.getTraceSegment();
Map<String, Object> tags = spanInfo.getTags();

Expand All @@ -861,8 +863,11 @@ private NoopFlow onRequestEnded(RequestContext ctx_, IGSpanInfo spanInfo) {

// AppSec report metric and events for web span only
if (traceSeg != null) {
traceSeg.setTagTop("_dd.appsec.enabled", 1);
traceSeg.setTagTop("_dd.runtime_family", "jvm");
// Set AppSec tags on the service-entry span (where detection occurs).
// When an inferred proxy span is present, InferredProxySpan.finish() will copy
// these tags to the inferred proxy span as required by RFC-1081.
span.setMetric("_dd.appsec.enabled", 1);
span.setTag("_dd.runtime_family", "jvm");

Collection<AppSecEvent> collectedEvents = ctx.transferCollectedEvents();

Expand All @@ -882,17 +887,22 @@ private NoopFlow onRequestEnded(RequestContext ctx_, IGSpanInfo spanInfo) {
traceSeg.setTagTop(Tags.ASM_KEEP, true);
traceSeg.setTagTop(Tags.PROPAGATED_TRACE_SOURCE, ProductTraceSource.ASM);
}
traceSeg.setTagTop("appsec.event", true);
traceSeg.setTagTop("network.client.ip", ctx.getPeerAddress());

span.setTag("appsec.event", true);

String peerAddress = ctx.getPeerAddress();
span.setTag("network.client.ip", peerAddress);

// Reflect client_ip as actor.ip for backward compatibility
Object clientIp = tags.get(Tags.HTTP_CLIENT_IP);
if (clientIp != null) {
traceSeg.setTagTop("actor.ip", clientIp);
span.setTag("actor.ip", clientIp.toString());
}

// Report AppSec events via "_dd.appsec.json" tag
// Report AppSec events on the service-entry span; also stored in meta_struct on the
// root span via setDataTop for agent processing
AppSecEventWrapper wrapper = new AppSecEventWrapper(collectedEvents);
span.setTag("_dd.appsec.json", wrapper);
traceSeg.setDataTop("appsec", wrapper);

// Report collected request and response headers based on allow list
Expand Down Expand Up @@ -1166,7 +1176,6 @@ public AppSecRequestContext getResult() {

private Flow<Void> maybePublishRequestData(AppSecRequestContext ctx) {
String savedRawURI = ctx.getSavedRawURI();

if (savedRawURI == null || !ctx.isFinishedRequestHeaders() || ctx.getPeerAddress() == null) {
return NoopFlow.INSTANCE;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ class AppSecSystemSpecification extends DDSpecification {
1 * appSecReqCtx.transferCollectedEvents() >> [Stub(AppSecEvent)]
1 * appSecReqCtx.getRequestHeaders() >> ['foo-bar': ['1.1.1.1']]
1 * appSecReqCtx.getResponseHeaders() >> [:]
1 * traceSegment.setTagTop('actor.ip', '1.1.1.1')
1 * span.setTag('actor.ip', '1.1.1.1')
}

void 'throws if the config file is not parseable'() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,13 +186,12 @@ class GatewayBridgeSpecification extends DDSpecification {
1 * mockAppSecCtx.transferCollectedEvents() >> [event]
1 * mockAppSecCtx.peerAddress >> '2001::1'
1 * mockAppSecCtx.close()
1 * traceSegment.setTagTop("_dd.appsec.enabled", 1)
1 * traceSegment.setTagTop("_dd.runtime_family", "jvm")
1 * traceSegment.setTagTop('appsec.event', true)
1 * spanInfo.setMetric("_dd.appsec.enabled", 1)
1 * spanInfo.setTag("_dd.runtime_family", "jvm")
1 * spanInfo.setTag('appsec.event', true)
1 * spanInfo.setTag('network.client.ip', '2001::1')
1 * spanInfo.setTag('actor.ip', '1.1.1.1')
1 * traceSegment.setDataTop('appsec', new AppSecEventWrapper([event]))
1 * traceSegment.setTagTop('http.request.headers.accept', 'header_value')
1 * traceSegment.setTagTop('http.response.headers.content-type', 'text/html; charset=UTF-8')
1 * traceSegment.setTagTop('network.client.ip', '2001::1')
1 * mockAppSecCtx.isWafBlocked()
1 * mockAppSecCtx.hasWafErrors()
1 * mockAppSecCtx.getWafTimeouts()
Expand Down Expand Up @@ -222,7 +221,7 @@ class GatewayBridgeSpecification extends DDSpecification {
then:
1 * mockAppSecCtx.transferCollectedEvents() >> [Stub(AppSecEvent)]
1 * spanInfo.getTags() >> TagMap.fromMap(['http.client_ip': '8.8.8.8'])
1 * traceSegment.setTagTop('actor.ip', '8.8.8.8')
1 * spanInfo.setTag('actor.ip', '8.8.8.8')
}

void 'bridge can collect headers'() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -517,8 +517,9 @@ class SpringBootBasedTest extends HttpServerTest<ConfigurableApplicationContext>
parent()
tags {
"$Tags.COMPONENT" "aws-apigateway"
"$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER
"$Tags.HTTP_METHOD" "GET"
"$Tags.HTTP_URL" "api.example.com/success"
"$Tags.HTTP_URL" "https://api.example.com/success"
"$Tags.HTTP_ROUTE" "/success"
"stage" "test"
"_dd.inferred_span" 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ class InferredProxyPropagatorTests {
private static final String PROXY_SYSTEM_KEY = "x-dd-proxy";
private static final String PROXY_REQUEST_TIME_MS_KEY = "x-dd-proxy-request-time-ms";
private static final String PROXY_PATH_KEY = "x-dd-proxy-path";
private static final String PROXY_RESOURCE_PATH_KEY = "x-dd-proxy-resource-path";
private static final String PROXY_HTTP_METHOD_KEY = "x-dd-proxy-httpmethod";
private static final String PROXY_DOMAIN_NAME_KEY = "x-dd-proxy-domain-name";
private static final MapVisitor MAP_VISITOR = new MapVisitor();
Expand Down Expand Up @@ -86,6 +87,64 @@ static Stream<Arguments> invalidOrMissingHeadersProviderForPropagator() { // Ren
of("PROXY_REQUEST_TIME_MS_KEY missing", missingTime));
}

@Test
@DisplayName("Should extract x-dd-proxy-resource-path header when present")
void testResourcePathHeaderExtraction() {
Map<String, String> headers = new HashMap<>();
headers.put(PROXY_SYSTEM_KEY, "aws-apigateway");
headers.put(PROXY_REQUEST_TIME_MS_KEY, "12345");
headers.put(PROXY_PATH_KEY, "/api/users/123");
headers.put(PROXY_RESOURCE_PATH_KEY, "/api/users/{id}");
headers.put(PROXY_HTTP_METHOD_KEY, "GET");
headers.put(PROXY_DOMAIN_NAME_KEY, "api.example.com");

Context context = this.propagator.extract(root(), headers, MAP_VISITOR);
InferredProxySpan inferredProxySpan = fromContext(context);
assertNotNull(inferredProxySpan);
assertTrue(inferredProxySpan.isValid());

// The resourcePath header should be extracted and available
// for use in http.route and resource.name
}

@Test
@DisplayName("Should work without x-dd-proxy-resource-path header for backwards compatibility")
void testExtractionWithoutResourcePath() {
Map<String, String> headers = new HashMap<>();
headers.put(PROXY_SYSTEM_KEY, "aws-apigateway");
headers.put(PROXY_REQUEST_TIME_MS_KEY, "12345");
headers.put(PROXY_PATH_KEY, "/api/users/123");
// No PROXY_RESOURCE_PATH_KEY
headers.put(PROXY_HTTP_METHOD_KEY, "GET");
headers.put(PROXY_DOMAIN_NAME_KEY, "api.example.com");

Context context = this.propagator.extract(root(), headers, MAP_VISITOR);
InferredProxySpan inferredProxySpan = fromContext(context);
assertNotNull(inferredProxySpan);
assertTrue(inferredProxySpan.isValid());

// Should still be valid without resourcePath (backwards compatibility)
}

@Test
@DisplayName("Should extract x-dd-proxy-resource-path for aws-httpapi")
void testResourcePathHeaderExtractionForAwsHttpApi() {
Map<String, String> headers = new HashMap<>();
headers.put(PROXY_SYSTEM_KEY, "aws-httpapi");
headers.put(PROXY_REQUEST_TIME_MS_KEY, "12345");
headers.put(PROXY_PATH_KEY, "/v2/items/abc-123");
headers.put(PROXY_RESOURCE_PATH_KEY, "/v2/items/{itemId}");
headers.put(PROXY_HTTP_METHOD_KEY, "POST");
headers.put(PROXY_DOMAIN_NAME_KEY, "httpapi.example.com");

Context context = this.propagator.extract(root(), headers, MAP_VISITOR);
InferredProxySpan inferredProxySpan = fromContext(context);
assertNotNull(inferredProxySpan);
assertTrue(inferredProxySpan.isValid());

// aws-httpapi should also support resourcePath extraction
}

@ParametersAreNonnullByDefault
private static class MapVisitor implements CarrierVisitor<Map<String, String>> {
@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_METHOD;
import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_ROUTE;
import static datadog.trace.bootstrap.instrumentation.api.Tags.HTTP_URL;
import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND;
import static datadog.trace.bootstrap.instrumentation.api.Tags.SPAN_KIND_SERVER;

import datadog.context.Context;
import datadog.context.ContextKey;
Expand All @@ -25,15 +27,21 @@ public class InferredProxySpan implements ImplicitContextKeyed {
static final String PROXY_SYSTEM = "x-dd-proxy";
static final String PROXY_START_TIME_MS = "x-dd-proxy-request-time-ms";
static final String PROXY_PATH = "x-dd-proxy-path";
static final String PROXY_RESOURCE_PATH = "x-dd-proxy-resource-path";
static final String PROXY_HTTP_METHOD = "x-dd-proxy-httpmethod";
static final String PROXY_DOMAIN_NAME = "x-dd-proxy-domain-name";
static final String STAGE = "x-dd-proxy-stage";
// Optional tags
static final String PROXY_ACCOUNT_ID = "x-dd-proxy-account-id";
static final String PROXY_API_ID = "x-dd-proxy-api-id";
static final String PROXY_REGION = "x-dd-proxy-region";
static final Map<String, String> SUPPORTED_PROXIES;
static final String INSTRUMENTATION_NAME = "inferred_proxy";

static {
SUPPORTED_PROXIES = new HashMap<>();
SUPPORTED_PROXIES.put("aws-apigateway", "aws.apigateway");
SUPPORTED_PROXIES.put("aws-httpapi", "aws.httpapi");
}

private final Map<String, String> headers;
Expand Down Expand Up @@ -75,6 +83,7 @@ public AgentSpanContext start(AgentSpanContext extracted) {
String proxy = SUPPORTED_PROXIES.get(proxySystem);
String httpMethod = header(PROXY_HTTP_METHOD);
String path = header(PROXY_PATH);
String resourcePath = header(PROXY_RESOURCE_PATH);
String domainName = header(PROXY_DOMAIN_NAME);

AgentSpan span = AgentTracer.get().startSpan(INSTRUMENTATION_NAME, proxy, extracted, startTime);
Expand All @@ -84,30 +93,62 @@ public AgentSpanContext start(AgentSpanContext extracted) {
domainName != null && !domainName.isEmpty() ? domainName : Config.get().getServiceName();
span.setServiceName(serviceName);

// Component: aws-apigateway
// Component: aws-apigateway or aws-httpapi
span.setTag(COMPONENT, proxySystem);

// Span kind: server
span.setTag(SPAN_KIND, SPAN_KIND_SERVER);

// SpanType: web
span.setTag(SPAN_TYPE, "web");

// Http.method - value of x-dd-proxy-httpmethod
span.setTag(HTTP_METHOD, httpMethod);

// Http.url - value of x-dd-proxy-domain-name + x-dd-proxy-path
span.setTag(HTTP_URL, domainName != null ? domainName + path : path);
// Http.url - https:// + x-dd-proxy-domain-name + x-dd-proxy-path
span.setTag(
HTTP_URL,
domainName != null && !domainName.isEmpty() ? "https://" + domainName + path : path);

// Http.route - value of x-dd-proxy-path
span.setTag(HTTP_ROUTE, path);
// Http.route - value of x-dd-proxy-resource-path (or x-dd-proxy-path as fallback)
span.setTag(HTTP_ROUTE, resourcePath != null && !resourcePath.isEmpty() ? resourcePath : path);

// "stage" - value of x-dd-proxy-stage
span.setTag("stage", header(STAGE));

// Optional tags - only set if present
String accountId = header(PROXY_ACCOUNT_ID);
if (accountId != null && !accountId.isEmpty()) {
span.setTag("account_id", accountId);
}

String apiId = header(PROXY_API_ID);
if (apiId != null && !apiId.isEmpty()) {
span.setTag("apiid", apiId);
}

String region = header(PROXY_REGION);
if (region != null && !region.isEmpty()) {
span.setTag("region", region);
}

// Compute and set dd_resource_key (ARN) if we have region and apiId
if (region != null && !region.isEmpty() && apiId != null && !apiId.isEmpty()) {
String arn = computeArn(proxySystem, region, apiId);
if (arn != null) {
span.setTag("dd_resource_key", arn);
}
}

// _dd.inferred_span = 1 (indicates that this is an inferred span)
span.setTag("_dd.inferred_span", 1);

// Resource Name: value of x-dd-proxy-httpmethod + " " + value of x-dd-proxy-path
// Resource Name: <Method> <Route> when route available, else <Method> <Path>
// Prefer x-dd-proxy-resource-path (route) over x-dd-proxy-path (path)
// Use MANUAL_INSTRUMENTATION priority to prevent TagInterceptor from overriding
String resourceName = httpMethod != null && path != null ? httpMethod + " " + path : null;
String routeOrPath = resourcePath != null && !resourcePath.isEmpty() ? resourcePath : path;
String resourceName =
httpMethod != null && routeOrPath != null ? httpMethod + " " + routeOrPath : null;
if (resourceName != null) {
span.setResourceName(resourceName, MANUAL_INSTRUMENTATION);
}
Expand All @@ -124,13 +165,72 @@ private String header(String name) {
return this.headers.get(name);
}

/**
* Compute ARN for the API Gateway resource. Format for v1 REST:
* arn:aws:apigateway:{region}::/restapis/{api-id} Format for v2 HTTP:
* arn:aws:apigateway:{region}::/apis/{api-id}
*/
private String computeArn(String proxySystem, String region, String apiId) {
if (proxySystem == null || region == null || apiId == null) {
return null;
}

// Assume AWS partition (could be extended to support other partitions like aws-cn, aws-us-gov)
String partition = "aws";

// Determine resource type based on proxy system
String resourceType;
if ("aws-apigateway".equals(proxySystem)) {
resourceType = "restapis"; // v1 REST API
} else if ("aws-httpapi".equals(proxySystem)) {
resourceType = "apis"; // v2 HTTP API
} else {
return null; // Unknown proxy type
}

return String.format("arn:%s:apigateway:%s::/%s/%s", partition, region, resourceType, apiId);
}

public void finish() {
finish(null);
}

/**
* Finishes this inferred proxy span and copies AppSec tags from the service-entry span to this
* span as required by RFC-1081. AppSec detection occurs in the service-entry span context, so its
* tags must be propagated to the inferred proxy span for endpoint correlation.
*
* @param serviceEntrySpan the service-entry child span, or null if not available
*/
public void finish(AgentSpan serviceEntrySpan) {
if (this.span != null) {
copyAppSecTagsFromServiceEntry(serviceEntrySpan);
this.span.finish();
this.span = null;
}
}

/**
* Copies AppSec tags from the service-entry span to this inferred proxy span as required by
* RFC-1081: the inferred span must carry {@code _dd.appsec.enabled} and {@code _dd.appsec.json}
* so that security activity can be correlated with the API Gateway endpoint.
*/
private void copyAppSecTagsFromServiceEntry(AgentSpan serviceEntrySpan) {
if (serviceEntrySpan == null || serviceEntrySpan == this.span) {
return;
}

Object appsecEnabled = serviceEntrySpan.getTag("_dd.appsec.enabled");
if (appsecEnabled != null) {
this.span.setMetric("_dd.appsec.enabled", 1);
}

Object appsecJson = serviceEntrySpan.getTag("_dd.appsec.json");
if (appsecJson != null) {
this.span.setTag("_dd.appsec.json", appsecJson.toString());
}
}

@Override
public Context storeInto(@Nonnull Context context) {
return context.with(CONTEXT_KEY, this);
Expand Down
Loading