Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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 @@ -288,6 +288,34 @@ void appSecCallbacksAreNotInvokedWhenAppSecIsDisabled() throws IOException {
assertTraces(trace(span().type(DDSpanTypes.SERVERLESS).error(false)));
}

@Test
void appSecIsSkippedAndReportedUnsupportedForNonHttpEvent() throws IOException {
String eventJson = "{\"Records\": [{\"eventSource\": \"aws:sqs\", \"body\": \"hello\"}]}";

ByteArrayInputStream input =
new ByteArrayInputStream(eventJson.getBytes(StandardCharsets.UTF_8));
ByteArrayOutputStream output = new ByteArrayOutputStream();
new HandlerStreaming().handleRequest(input, output, newContext());

assertFalse(appSecStarted);
assertNull(capturedMethod);
assertNull(capturedPath);
assertTrue(capturedHeaders.isEmpty());
assertNull(capturedBody);
assertFalse(appSecEnded);
assertNull(capturedResponseStatus);
// Tag matching is exhaustive, so this also asserts the span carries no http.* tag
assertTraces(
trace(
span()
.type(DDSpanTypes.SERVERLESS)
.error(false)
.tags(
defaultTags(),
tag("request_id", is(REQUEST_ID)),
tag("_dd.appsec.unsupported_event_type", is(1)))));
}

@Test
void responseCallbacksAreInvokedForJsonEncodedResponse() throws IOException {
String eventJson =
Expand Down Expand Up @@ -382,7 +410,8 @@ void responseCallbacksSkipNonApiGatewayResponseForNonHttpEvent() throws IOExcept
assertTrue(capturedResponseHeaders.isEmpty());
assertNull(capturedResponseBody);
assertFalse(responseHeaderDoneCalled);
assertTrue(appSecEnded);
// AppSec skipped the invocation entirely, so there is no request context to end
assertFalse(appSecEnded);
assertTraces(trace(span().type(DDSpanTypes.SERVERLESS).error(false)));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ public class LambdaAppSecHandler {
private static final Logger log = LoggerFactory.getLogger(LambdaAppSecHandler.class);
private static final RatelimitedLogger rlLog = new RatelimitedLogger(log, 5, TimeUnit.MINUTES);

/** Marks an invocation AppSec did not process because the trigger is not HTTP. */
private static final String UNSUPPORTED_EVENT_TYPE_METRIC = "_dd.appsec.unsupported_event_type";

// Carries the detected trigger type from processRequestStart to processResponseData within the
// same Lambda invocation. Cleared in processRequestEnd.
private static final ThreadLocal<LambdaTriggerType> CURRENT_TRIGGER_TYPE = new ThreadLocal<>();
Expand All @@ -68,7 +71,8 @@ public class LambdaAppSecHandler {
*
* @param event the Lambda event object
* @return a {@link TagContext} carrying the AppSec request context and the HTTP tags, or null if
* AppSec is disabled, the event is not a parseable payload, or processing fails
* AppSec is disabled, the trigger is not HTTP, the event is not a parseable payload, or
* processing fails
*/
public static AgentSpanContext processRequestStart(Object event) {
if (!ActiveSubsystems.APPSEC_ACTIVE) {
Expand All @@ -91,6 +95,11 @@ public static AgentSpanContext processRequestStart(Object event) {
return null;
}
CURRENT_TRIGGER_TYPE.set(eventData.triggerType);
if (!eventData.triggerType.isHttp()) {
log.debug("Trigger type {} is not HTTP, skipping AppSec processing", eventData.triggerType);
// unsupported event metric is added on request end since span doesn't exist yet
return null;
}
// v2 payloads carry the request line verbatim; the others expose the path and a decoded
// parameter map only, so the query string has to be rebuilt from them
String fullPath = eventData.rawUri;
Expand All @@ -100,7 +109,7 @@ public static AgentSpanContext processRequestStart(Object event) {
LambdaURIDataAdapter uriAdapter =
new LambdaURIDataAdapter(fullPath, eventData.headers, eventData.host);
AgentSpanContext context = processAppSecRequestData(eventData, uriAdapter);
if (context instanceof TagContext && eventData.triggerType.isHttp()) {
if (context instanceof TagContext) {
applyHttpTags((TagContext) context, eventData, uriAdapter);
}
return context;
Expand All @@ -117,12 +126,20 @@ public static AgentSpanContext processRequestStart(Object event) {
* @param span the current span
*/
public static void processRequestEnd(AgentSpan span) {
LambdaTriggerType triggerType = CURRENT_TRIGGER_TYPE.get();
CURRENT_TRIGGER_TYPE.remove();

if (!ActiveSubsystems.APPSEC_ACTIVE || span == null) {
return;
}

// A null trigger type means processRequestStart never ran, so the invocation was not analysed
// at all, which is not the same as an unsupported trigger.
if (triggerType != null && !triggerType.isHttp()) {
span.setMetric(UNSUPPORTED_EVENT_TYPE_METRIC, 1);
Comment thread
claponcet marked this conversation as resolved.
return;
}

RequestContext requestContext = span.getRequestContext();
if (requestContext != null) {
AgentTracer.TracerAPI tracer = AgentTracer.get();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ static LambdaRequestData parseEvent(String json) {
case ALB_MULTI_VALUE:
return extractAlbData(event, triggerType);
default:
log.debug("Unknown trigger type, attempting generic extraction");
return extractGenericData(event);
// Unsupported trigger: AppSec skips the invocation entirely, so there is nothing to
// extract. The trigger type is carried by the caller, not by this result.
return LambdaRequestData.EMPTY;
}
} catch (Exception e) {
log.debug("Failed to parse event data from JSON", e);
Expand Down Expand Up @@ -444,69 +445,6 @@ private static LambdaRequestData extractAlbData(
null);
}

/** Generic data extraction for unknown trigger types (fallback) */
private static LambdaRequestData extractGenericData(Map<String, Object> event) {
Map<String, String> headers = extractHeadersWithCookies(event);
Map<String, String> pathParameters = extractPathParameters(event.get("pathParameters"));
Map<String, List<String>> queryParameters =
extractQueryParameters(event.get("queryStringParameters"));
Object body = extractBody(event);

String method = null;
String path = null;
String sourceIp = null;

// Try to extract from requestContext if available
Object requestContextObj = event.get("requestContext");
if (requestContextObj instanceof Map) {
Map<?, ?> requestContext = (Map<?, ?>) requestContextObj;

Object httpObj = requestContext.get("http");
if (httpObj instanceof Map) {
Map<?, ?> http = (Map<?, ?>) httpObj;
method = (String) http.get("method");
path = (String) http.get("path");
sourceIp = (String) http.get("sourceIp");
} else {
Object methodObj = requestContext.get("httpMethod");
if (methodObj != null) {
method = String.valueOf(methodObj);
}

Object identityObj = requestContext.get("identity");
if (identityObj instanceof Map) {
Map<?, ?> identity = (Map<?, ?>) identityObj;
sourceIp = (String) identity.get("sourceIp");
}
}
}

// Try root level fields
if (method == null) {
Object methodObj = event.get("httpMethod");
if (methodObj != null) {
method = String.valueOf(methodObj);
}
}
if (path == null) {
Object pathObj = event.get("path");
if (pathObj != null) {
path = String.valueOf(pathObj);
}
}

return new LambdaRequestData(
headers,
method,
path,
sourceIp,
null,
LambdaTriggerType.UNKNOWN,
pathParameters,
queryParameters,
body);
}

/**
* Looks a header up in a map produced by {@link #extractHeaders}, whose keys are already
* lowercased, so {@code lowerCaseName} must be lowercase for a match.
Expand Down Expand Up @@ -790,8 +728,22 @@ enum LambdaTriggerType {
LAMBDA_URL, // Lambda Function URL
UNKNOWN; // Unknown or unsupported trigger

/**
* Whitelist rather than {@code != UNKNOWN} so a trigger type added later defaults to non-HTTP,
* and therefore to being skipped by AppSec, until it is deliberately listed here.
*/
boolean isHttp() {
return this != UNKNOWN;
switch (this) {
case API_GATEWAY_V1_REST:
case API_GATEWAY_V2_HTTP:
case API_GATEWAY_V2_WEBSOCKET:
case ALB:
case ALB_MULTI_VALUE:
case LAMBDA_URL:
return true;
default:
return false;
}
}
}

Expand Down
Loading