diff --git a/src/trace/listener.spec.ts b/src/trace/listener.spec.ts index 1e8dc189..37bd4aa4 100644 --- a/src/trace/listener.spec.ts +++ b/src/trace/listener.spec.ts @@ -16,6 +16,7 @@ let mockExtract: jest.Mock; let mockSpanContextWrapper: any; let mockSpanContext: any; let mockTraceSource: TraceSource | undefined = undefined; +let mockCurrentSpan: any = null; jest.mock("./tracer-wrapper", () => { mockWrap = jest.fn().mockImplementation((name, options, func) => func); @@ -25,6 +26,10 @@ jest.mock("./tracer-wrapper", () => { constructor() {} + get currentSpan(): any { + return mockCurrentSpan; + } + wrap(name: any, options: any, fn: any): any { return mockWrap(name, options, fn); } @@ -560,4 +565,97 @@ describe("TraceListener", () => { ); }); }); + + describe("AppSec tag propagation", () => { + beforeEach(() => { + mockWrap.mockClear(); + mockExtract.mockClear(); + mockSpanContext = undefined; + mockSpanContextWrapper = undefined; + mockTraceSource = undefined; + mockCurrentSpan = null; + process.env = { ...oldEnv }; + }); + + afterEach(() => { + process.env = oldEnv; + mockCurrentSpan = null; + }); + + it("copies _dd.appsec.json from lambda span to inferred span when present", async () => { + const mockSetTag = jest.fn(); + const appsecJsonValue = '{"triggers":[{"rule":{"id":"rule-1"}}]}'; + + mockCurrentSpan = { + _tags: { + "_dd.appsec.json": appsecJsonValue, + }, + }; + + const listener = new TraceListener(defaultConfig); + await listener.onStartInvocation({}, context as any); + + const mockInferredSpan = { + isAsync: () => false, + setTag: mockSetTag, + finish: jest.fn(), + }; + (listener as any).inferredSpan = mockInferredSpan; + + const unwrappedFunc = () => {}; + const wrappedFunc = listener.onWrap(unwrappedFunc); + wrappedFunc(); + await listener.onCompleteInvocation(); + + expect(mockSetTag).toHaveBeenCalledWith("_dd.appsec.json", appsecJsonValue); + }); + + it("does not set _dd.appsec.json on inferred span when not present on lambda span", async () => { + const mockSetTag = jest.fn(); + + mockCurrentSpan = { + _tags: {}, + }; + + const listener = new TraceListener(defaultConfig); + await listener.onStartInvocation({}, context as any); + + const mockInferredSpan = { + isAsync: () => false, + setTag: mockSetTag, + finish: jest.fn(), + }; + (listener as any).inferredSpan = mockInferredSpan; + + const unwrappedFunc = () => {}; + const wrappedFunc = listener.onWrap(unwrappedFunc); + wrappedFunc(); + await listener.onCompleteInvocation(); + + expect(mockSetTag).not.toHaveBeenCalledWith("_dd.appsec.json", expect.anything()); + }); + + it("does not set _dd.appsec.json when lambda span is not available", async () => { + const mockSetTag = jest.fn(); + + mockCurrentSpan = null; + + const listener = new TraceListener(defaultConfig); + await listener.onStartInvocation({}, context as any); + + const mockInferredSpan = { + isAsync: () => false, + setTag: mockSetTag, + finish: jest.fn(), + }; + (listener as any).inferredSpan = mockInferredSpan; + + const unwrappedFunc = () => {}; + const wrappedFunc = listener.onWrap(unwrappedFunc); + wrappedFunc(); + await listener.onCompleteInvocation(); + + expect(mockSetTag).not.toHaveBeenCalledWith("_dd.appsec.json", expect.anything()); + }); + }); }); diff --git a/src/trace/listener.ts b/src/trace/listener.ts index 1e312a71..2be06a2b 100644 --- a/src/trace/listener.ts +++ b/src/trace/listener.ts @@ -267,6 +267,15 @@ export class TraceListener { logDebug("Setting error tag to inferred span"); this.inferredSpan.setTag("error", error); } + + const lambdaSpan = this.tracerWrapper.currentSpan; + if (lambdaSpan) { + const appsecJson = lambdaSpan._tags?.["_dd.appsec.json"]; + if (appsecJson) { + this.inferredSpan.setTag("_dd.appsec.json", appsecJson); + } + } + if (this.inferredSpan.isAsync()) { finishTime = this.wrappedCurrentSpan?.startTime() || Date.now(); } else { diff --git a/src/trace/span-inferrer.spec.ts b/src/trace/span-inferrer.spec.ts index 19eb5709..674eb1a3 100644 --- a/src/trace/span-inferrer.spec.ts +++ b/src/trace/span-inferrer.spec.ts @@ -891,7 +891,6 @@ describe("SpanInferrer", () => { event_type: "CONNECT", "http.url": "https://08se3mvh28.execute-api.eu-west-1.amazonaws.com$connect", message_direction: "IN", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "08se3mvh28.execute-api.eu-west-1.amazonaws.com $connect", @@ -899,7 +898,7 @@ describe("SpanInferrer", () => { service: "08se3mvh28.execute-api.eu-west-1.amazonaws.com", "service.name": "08se3mvh28.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", }, }); }); @@ -917,7 +916,6 @@ describe("SpanInferrer", () => { endpoint: "/my/path", "http.url": "https://id.execute-api.us-east-1.amazonaws.com/my/path", domain_name: "id.execute-api.us-east-1.amazonaws.com", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "http.method": "GET", @@ -926,7 +924,7 @@ describe("SpanInferrer", () => { service: "id.execute-api.us-east-1.amazonaws.com", "service.name": "id.execute-api.us-east-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "$default", }, }); @@ -945,7 +943,6 @@ describe("SpanInferrer", () => { endpoint: "/default/nodejs-apig-function-1G3XMPLZXVXYI", "http.url": "https://r3pmxmplak.execute-api.us-east-2.amazonaws.com/default/nodejs-apig-function-1G3XMPLZXVXYI", domain_name: "r3pmxmplak.execute-api.us-east-2.amazonaws.com", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "http.method": "GET", @@ -954,7 +951,7 @@ describe("SpanInferrer", () => { service: "r3pmxmplak.execute-api.us-east-2.amazonaws.com", "service.name": "r3pmxmplak.execute-api.us-east-2.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "default", }, }); @@ -973,7 +970,6 @@ describe("SpanInferrer", () => { endpoint: "/dev/user/42", "http.url": "https://mcwkra0ya4.execute-api.sa-east-1.amazonaws.com/dev/user/42", domain_name: "mcwkra0ya4.execute-api.sa-east-1.amazonaws.com", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "http.method": "GET", @@ -982,7 +978,7 @@ describe("SpanInferrer", () => { service: "mcwkra0ya4.execute-api.sa-east-1.amazonaws.com", "service.name": "mcwkra0ya4.execute-api.sa-east-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "dev", }, }); @@ -1001,7 +997,6 @@ describe("SpanInferrer", () => { endpoint: "/user/42", "http.url": "https://9vj54we5ih.execute-api.sa-east-1.amazonaws.com/user/42", domain_name: "9vj54we5ih.execute-api.sa-east-1.amazonaws.com", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "http.method": "GET", @@ -1010,7 +1005,7 @@ describe("SpanInferrer", () => { service: "9vj54we5ih.execute-api.sa-east-1.amazonaws.com", "service.name": "9vj54we5ih.execute-api.sa-east-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "$default", }, }); @@ -1071,6 +1066,36 @@ describe("SpanInferrer", () => { }, }); }); + + it("includes _dd.appsec.enabled tag when DD_APPSEC_ENABLED is true", () => { + process.env.DD_APPSEC_ENABLED = "true"; + const inferrer = new SpanInferrer(mockWrapper as unknown as TracerWrapper); + inferrer.createInferredSpan(apiGatewayV1, {} as any, {} as SpanContext); + + const callArgs = mockWrapper.startSpan.mock.calls[0]; + expect(callArgs[1].tags["_dd.appsec.enabled"]).toBe(1); + delete process.env.DD_APPSEC_ENABLED; + }); + + it("does not include _dd.appsec.enabled tag when DD_APPSEC_ENABLED is not set", () => { + delete process.env.DD_APPSEC_ENABLED; + delete process.env.DD_SERVERLESS_APPSEC_ENABLED; + const inferrer = new SpanInferrer(mockWrapper as unknown as TracerWrapper); + inferrer.createInferredSpan(apiGatewayV1, {} as any, {} as SpanContext); + + const callArgs = mockWrapper.startSpan.mock.calls[0]; + expect(callArgs[1].tags["_dd.appsec.enabled"]).toBeUndefined(); + }); + + it("includes _dd.appsec.enabled tag when DD_SERVERLESS_APPSEC_ENABLED is true", () => { + process.env.DD_SERVERLESS_APPSEC_ENABLED = "true"; + const inferrer = new SpanInferrer(mockWrapper as unknown as TracerWrapper); + inferrer.createInferredSpan(apiGatewayV1, {} as any, {} as SpanContext); + + const callArgs = mockWrapper.startSpan.mock.calls[0]; + expect(callArgs[1].tags["_dd.appsec.enabled"]).toBe(1); + delete process.env.DD_SERVERLESS_APPSEC_ENABLED; + }); }); const mockFinish = () => undefined; @@ -1118,7 +1143,6 @@ describe("Authorizer Spans", () => { endpoint: "/dev/hello", "http.method": "POST", "http.url": "https://3gsxz7lha4.execute-api.eu-west-1.amazonaws.com/dev/hello", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "POST /hello", @@ -1126,7 +1150,7 @@ describe("Authorizer Spans", () => { service: "3gsxz7lha4.execute-api.eu-west-1.amazonaws.com", "service.name": "3gsxz7lha4.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "dev", }, }, @@ -1143,7 +1167,6 @@ describe("Authorizer Spans", () => { endpoint: "/dev/hello", "http.method": "POST", "http.url": "https://3gsxz7lha4.execute-api.eu-west-1.amazonaws.com/dev/hello", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "POST /hello", @@ -1151,7 +1174,7 @@ describe("Authorizer Spans", () => { service: "3gsxz7lha4.execute-api.eu-west-1.amazonaws.com", "service.name": "3gsxz7lha4.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "dev", }, }, @@ -1173,7 +1196,6 @@ describe("Authorizer Spans", () => { endpoint: "/dev/hello", "http.method": "POST", "http.url": "https://3gsxz7lha4.execute-api.eu-west-1.amazonaws.com/dev/hello", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "POST /hello", @@ -1181,7 +1203,7 @@ describe("Authorizer Spans", () => { service: "3gsxz7lha4.execute-api.eu-west-1.amazonaws.com", "service.name": "3gsxz7lha4.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "dev", }, }, @@ -1203,7 +1225,6 @@ describe("Authorizer Spans", () => { endpoint: "/dev/hi", "http.method": "GET", "http.url": "https://4dyr9xqip7.execute-api.eu-west-1.amazonaws.com/dev/hi", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "GET /hi", @@ -1211,7 +1232,7 @@ describe("Authorizer Spans", () => { service: "4dyr9xqip7.execute-api.eu-west-1.amazonaws.com", "service.name": "4dyr9xqip7.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "dev", }, }, @@ -1228,7 +1249,6 @@ describe("Authorizer Spans", () => { endpoint: "/dev/hi", "http.method": "GET", "http.url": "https://4dyr9xqip7.execute-api.eu-west-1.amazonaws.com/dev/hi", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "GET /hi", @@ -1236,7 +1256,7 @@ describe("Authorizer Spans", () => { service: "4dyr9xqip7.execute-api.eu-west-1.amazonaws.com", "service.name": "4dyr9xqip7.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "dev", }, }, @@ -1258,7 +1278,6 @@ describe("Authorizer Spans", () => { endpoint: "/dev/hi", "http.method": "GET", "http.url": "https://4dyr9xqip7.execute-api.eu-west-1.amazonaws.com/dev/hi", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "GET /hi", @@ -1266,7 +1285,7 @@ describe("Authorizer Spans", () => { service: "4dyr9xqip7.execute-api.eu-west-1.amazonaws.com", "service.name": "4dyr9xqip7.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "dev", }, }, @@ -1288,7 +1307,6 @@ describe("Authorizer Spans", () => { endpoint: "/hello", "http.method": "GET", "http.url": "https://l9flvsey83.execute-api.eu-west-1.amazonaws.com/hello", - operation_name: "aws.httpapi", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "GET /hello", @@ -1296,7 +1314,7 @@ describe("Authorizer Spans", () => { service: "l9flvsey83.execute-api.eu-west-1.amazonaws.com", "service.name": "l9flvsey83.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "$default", }, }, @@ -1318,7 +1336,6 @@ describe("Authorizer Spans", () => { endpoint: "/hello", "http.method": "GET", "http.url": "https://l9flvsey83.execute-api.eu-west-1.amazonaws.com/hello", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "GET /hello", @@ -1326,7 +1343,7 @@ describe("Authorizer Spans", () => { service: "l9flvsey83.execute-api.eu-west-1.amazonaws.com", "service.name": "l9flvsey83.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", stage: "$default", }, }, @@ -1349,7 +1366,6 @@ describe("Authorizer Spans", () => { event_type: "CONNECT", "http.url": "https://85fj5nw29d.execute-api.eu-west-1.amazonaws.com$connect", message_direction: "IN", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "85fj5nw29d.execute-api.eu-west-1.amazonaws.com $connect", @@ -1357,7 +1373,7 @@ describe("Authorizer Spans", () => { service: "85fj5nw29d.execute-api.eu-west-1.amazonaws.com", "service.name": "85fj5nw29d.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", }, }, ]); @@ -1374,7 +1390,6 @@ describe("Authorizer Spans", () => { event_type: "CONNECT", "http.url": "https://85fj5nw29d.execute-api.eu-west-1.amazonaws.com$connect", message_direction: "IN", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "85fj5nw29d.execute-api.eu-west-1.amazonaws.com $connect", @@ -1382,7 +1397,7 @@ describe("Authorizer Spans", () => { service: "85fj5nw29d.execute-api.eu-west-1.amazonaws.com", "service.name": "85fj5nw29d.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", }, }, ]); @@ -1404,7 +1419,6 @@ describe("Authorizer Spans", () => { event_type: "MESSAGE", "http.url": "https://85fj5nw29d.execute-api.eu-west-1.amazonaws.comhello", message_direction: "IN", - operation_name: "aws.apigateway", "peer.service": "mock-lambda-service", request_id: undefined, "resource.name": "85fj5nw29d.execute-api.eu-west-1.amazonaws.com hello", @@ -1412,7 +1426,7 @@ describe("Authorizer Spans", () => { service: "85fj5nw29d.execute-api.eu-west-1.amazonaws.com", "service.name": "85fj5nw29d.execute-api.eu-west-1.amazonaws.com", "span.kind": "server", - "span.type": "http", + "span.type": "web", }, }, ]); diff --git a/src/trace/span-inferrer.ts b/src/trace/span-inferrer.ts index 34466006..c1bc5719 100644 --- a/src/trace/span-inferrer.ts +++ b/src/trace/span-inferrer.ts @@ -97,6 +97,10 @@ export class SpanInferrer { return extractedKey?.trim() ? extractedKey : fallback; } + private static isAppsecEnabled(): boolean { + return process.env.DD_APPSEC_ENABLED === "true" || process.env.DD_SERVERLESS_APPSEC_ENABLED === "true"; + } + createInferredSpanForApiGateway( event: any, context: Context | undefined, @@ -120,14 +124,13 @@ export class SpanInferrer { const serviceName = SpanInferrer.determineServiceName(apiId, "lambda_api_gateway", domain, domain); options.tags = { - operation_name: "aws.apigateway", "http.url": httpUrl, endpoint: path, resource_names: resourceName, request_id: context?.awsRequestId, service: serviceName, "service.name": serviceName, - "span.type": "http", + "span.type": "web", "resource.name": resourceName, "peer.service": this.service, "span.kind": "server", @@ -149,6 +152,10 @@ export class SpanInferrer { options.tags.connection_id = event.requestContext.connectionId; options.tags.event_type = event.requestContext.eventType; } + + if (SpanInferrer.isAppsecEnabled()) { + options.tags["_dd.appsec.enabled"] = 1; + } let upstreamAuthorizerSpan: SpanWrapper | undefined; const eventSourceSubType: HTTPEventSubType = HTTPEventTraceExtractor.getEventSubType(event); if (decodeAuthorizerContext) { @@ -160,12 +167,11 @@ export class SpanInferrer { // getting an approximated endTime if (eventSourceSubType === HTTPEventSubType.ApiGatewayV2) { options.startTime = startTime; // not inserting authorizer span - options.tags.operation_name = "aws.httpapi"; } else { upstreamSpanOptions = { startTime, childOf: parentSpanContext, - tags: { operation_name: "aws.apigateway.authorizer", ...options.tags }, + tags: { ...options.tags }, }; upstreamAuthorizerSpan = new SpanWrapper( this.traceWrapper.startSpan("aws.apigateway.authorizer", upstreamSpanOptions),