Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
a9fffb5
Remove unused native-appsec binaries + native-appsec version pinned i…
CarlesDD Mar 16, 2026
6f6644e
Extract event data and pass it over dc
CarlesDD Mar 16, 2026
2c02b6e
Fix linting issues
CarlesDD Mar 16, 2026
9d54432
Fix formatting issue
CarlesDD Mar 16, 2026
ccda597
Fix test mock
CarlesDD Mar 16, 2026
84f05f2
Fix typo. Add test for Appsec integration in listener
CarlesDD Apr 21, 2026
896f723
Avoid including cookies and route when they are undefined
CarlesDD Apr 21, 2026
b0acc36
Rework for native-appsec package dependency
CarlesDD Apr 21, 2026
3241d3e
Clean format
CarlesDD Apr 21, 2026
7553eb6
Updated yarn.lock
CarlesDD Apr 21, 2026
a7f307e
Case insensitive enablement env var
CarlesDD Jun 16, 2026
52ced89
Fix Appsec reported path for APIGW v1
CarlesDD Jun 30, 2026
87cab59
Add tag for unsupported event type
CarlesDD Jun 30, 2026
9888635
Minor fixes
CarlesDD Jun 30, 2026
160cc17
update dd-trace-js
CarlesDD Jul 10, 2026
ef54676
fix(appsec): process AppSec request before invoking the user handler,…
CarlesDD Jul 13, 2026
fde79ca
fix(appsec): resolve DD_APPSEC_ENABLED through config instead of read…
CarlesDD Jul 16, 2026
c743a31
chore: regenerate yarn.lock for dd-trace ^5.113.0 after rebase
CarlesDD Jul 16, 2026
c79752d
refactor(appsec): gate on config.appsecEnabled at call sites, drop mo…
CarlesDD Jul 17, 2026
7eabe4a
refactor(appsec): extract response status/headers inside processAppse…
CarlesDD Jul 17, 2026
a0081d9
style: remove leftover blank lines in listener.ts
CarlesDD Jul 17, 2026
cfd7de1
chore: bump layer size limits to 9mb/23mb for @datadog/native-appsec
CarlesDD Jul 17, 2026
18d7d47
chore: bump layer size limits to 9mb/24mb for @datadog/native-appsec
CarlesDD Jul 17, 2026
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
12 changes: 11 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,13 @@ fs.writeFileSync(
EOF

RUN cp ./src/handler.mjs /nodejs/node_modules/datadog-lambda-js
RUN rm -rf node_modules

# Move dd-trace from devDependencies to production dependencies
# That way it is included in our layer, while keeping it an optional dependency for npm
RUN node ./scripts/move_ddtrace_dependency.js "$(cat package.json)" > package-new.json
RUN mv package-new.json package.json
RUN rm -rf node_modules

# Install dependencies
RUN yarn install --production=true --ignore-optional --ignore-engines
# Copy the dependencies to the modules folder
Expand All @@ -62,6 +63,15 @@ RUN rm -rf /nodejs/node_modules/@datadog/pprof/prebuilds/*/node-120.node
RUN rm -rf /nodejs/node_modules/@datadog/pprof/prebuilds/*/node-131.node
RUN rm -rf /nodejs/node_modules/@datadog/pprof/prebuilds/*/node-141.node

# Remove unused @datadog/native-appsec prebuilds for non-Lambda platforms.
# Lambda runs on Amazon Linux 2 (glibc), on x64 or arm64.
RUN rm -rf /nodejs/node_modules/@datadog/native-appsec/prebuilds/darwin-arm64
RUN rm -rf /nodejs/node_modules/@datadog/native-appsec/prebuilds/darwin-x64
RUN rm -rf /nodejs/node_modules/@datadog/native-appsec/prebuilds/win32-ia32
RUN rm -rf /nodejs/node_modules/@datadog/native-appsec/prebuilds/win32-x64
RUN rm -rf /nodejs/node_modules/@datadog/native-appsec/prebuilds/linuxmusl-arm64
RUN rm -rf /nodejs/node_modules/@datadog/native-appsec/prebuilds/linuxmusl-x64

# Remove heavy files from @opentelemetry/api which aren't used in a lambda environment.
# TODO: Create a completely separate Datadog scoped package for OpenTelemetry instead.
RUN rm -rf /nodejs/node_modules/@opentelemetry/api/build/esm
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"devDependencies": {
"@aws-sdk/client-kms": "^3.366.0",
"@aws-sdk/client-secrets-manager": "^3.721.0",
"@datadog/native-appsec": "*",
"@datadog/pprof": "5.14.4",
"@opentelemetry/api": ">=1.0.0 <1.10.0",
"@opentelemetry/api-logs": "<1.0.0",
Expand All @@ -32,7 +33,7 @@
"@types/node": "^20.12.10",
"@types/promise-retry": "^1.1.3",
"@types/shimmer": "^1.0.1",
"dd-trace": "^5.109.0",
"dd-trace": "^5.113.0",
"jest": "^27.0.1",
"mock-fs": "4.14.0",
"nock": "13.5.4",
Expand All @@ -43,7 +44,7 @@
},
"dependencies": {
"@aws-crypto/sha256-js": "5.2.0",
"dc-polyfill": "^0.1.3",
"dc-polyfill": "^0.1.11",
"promise-retry": "^2.0.1",
"serialize-error": "^8.1.0",
"shimmer": "1.2.1"
Expand Down
6 changes: 3 additions & 3 deletions scripts/check_layer_size.sh
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

# Compares layer size to threshold, and fails if below that threshold

# 7 mb size limit
MAX_LAYER_COMPRESSED_SIZE_KB=$(expr 7 \* 1024)
MAX_LAYER_UNCOMPRESSED_SIZE_KB=$(expr 19 \* 1024)
# 9 mb size limit
MAX_LAYER_COMPRESSED_SIZE_KB=$(expr 9 \* 1024)
MAX_LAYER_UNCOMPRESSED_SIZE_KB=$(expr 24 \* 1024)


LAYER_FILES_PREFIX="datadog_lambda_node"
Expand Down
1 change: 1 addition & 0 deletions scripts/move_ddtrace_dependency.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
const file = JSON.parse(process.argv[2]);

moveDependency('dd-trace')
moveDependency('@datadog/native-appsec')
moveDependency('@datadog/pprof')
moveDependency('@opentelemetry/api')
moveDependency('@opentelemetry/api-logs')
Expand Down
278 changes: 278 additions & 0 deletions src/appsec/event-data-extractor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
import { extractHTTPDataFromEvent } from "./event-data-extractor";

describe("extractHTTPDataFromEvent", () => {
describe("non-HTTP events", () => {
it("should return undefined for SQS events", () => {
const event = { Records: [{ eventSource: "aws:sqs", body: "test" }] };
expect(extractHTTPDataFromEvent(event)).toBeUndefined();
});

it("should return undefined for S3 events", () => {
const event = { Records: [{ s3: { bucket: { name: "test" } } }] };
expect(extractHTTPDataFromEvent(event)).toBeUndefined();
});

it("should return undefined for empty events", () => {
expect(extractHTTPDataFromEvent({})).toBeUndefined();
});
});

describe("API Gateway v1", () => {
const baseEvent = {
httpMethod: "GET",
path: "/my/path",
resource: "/my/{param}",
headers: { Host: "example.com", Cookie: "session=abc; lang=en" },
multiValueHeaders: null,
queryStringParameters: { foo: "bar" },
multiValueQueryStringParameters: null,
pathParameters: { param: "123" },
body: null,
isBase64Encoded: false,
requestContext: {
stage: "prod",
path: "/prod/my/path",
identity: { sourceIp: "1.2.3.4" },
},
};

it("should extract HTTP data correctly", () => {
const result = extractHTTPDataFromEvent(baseEvent);

expect(result).toBeDefined();
expect(result!.method).toBe("GET");
expect(result!.path).toBe("/prod/my/path");
expect(result!.clientIp).toBe("1.2.3.4");
expect(result!.route).toBe("/my/{param}");
expect(result!.pathParams).toEqual({ param: "123" });
expect(result!.query).toEqual({ foo: "bar" });
expect(result!.isBase64Encoded).toBe(false);
});

it("should separate cookies from headers", () => {
const result = extractHTTPDataFromEvent(baseEvent);

expect(result!.headers.cookie).toBeUndefined();
expect(result!.cookies).toEqual({ session: "abc", lang: "en" });
});

it("should normalize header names to lowercase", () => {
const result = extractHTTPDataFromEvent(baseEvent);

expect(result!.headers.host).toBe("example.com");
});

it("should decode base64 body", () => {
const event = {
...baseEvent,
body: Buffer.from('{"key":"value"}').toString("base64"),
isBase64Encoded: true,
};

const result = extractHTTPDataFromEvent(event);
expect(result!.body).toEqual({ key: "value" });
expect(result!.isBase64Encoded).toBe(true);
});

it("should parse JSON body when not base64 encoded", () => {
const event = {
...baseEvent,
body: '{"key":"value"}',
};

const result = extractHTTPDataFromEvent(event);
expect(result!.body).toEqual({ key: "value" });
});

it("should return raw string body when not JSON", () => {
const event = {
...baseEvent,
body: "plain text body",
};

const result = extractHTTPDataFromEvent(event);
expect(result!.body).toBe("plain text body");
});

it("should not include cookies when cookie header is absent", () => {
const event = {
...baseEvent,
headers: { Host: "example.com" },
};
const result = extractHTTPDataFromEvent(event);
expect(result!.cookies).toBeUndefined();
expect("cookies" in result!).toBe(false);
});

it("should not include route when resource is empty string", () => {
const event = { ...baseEvent, resource: "" };
const result = extractHTTPDataFromEvent(event);
expect(result!.route).toBeUndefined();
expect("route" in result!).toBe(false);
});

it("should merge multi-value query params", () => {
const event = {
...baseEvent,
queryStringParameters: { foo: "bar" },
multiValueQueryStringParameters: { foo: ["bar", "baz"], single: ["one"] },
};

const result = extractHTTPDataFromEvent(event);
expect(result!.query).toEqual({ foo: ["bar", "baz"], single: "one" });
});
});

describe("API Gateway v2", () => {
const baseEvent = {
version: "2.0",
rawPath: "/my/path",
rawQueryString: "foo=bar",
headers: { host: "example.com" },
queryStringParameters: { foo: "bar" },
pathParameters: { id: "456" },
body: null,
isBase64Encoded: false,
cookies: ["session=abc", "lang=en"],
routeKey: "GET /my/{id}",
requestContext: {
http: {
method: "POST",
path: "/my/path",
sourceIp: "5.6.7.8",
},
domainName: "api.example.com",
apiId: "abc123",
stage: "$default",
},
};

it("should extract HTTP data correctly", () => {
const result = extractHTTPDataFromEvent(baseEvent);

expect(result).toBeDefined();
expect(result!.method).toBe("POST");
expect(result!.path).toBe("/my/path");
expect(result!.clientIp).toBe("5.6.7.8");
expect(result!.route).toBe("/my/{id}");
expect(result!.pathParams).toEqual({ id: "456" });
});

it("should parse cookies from the cookies array", () => {
const result = extractHTTPDataFromEvent(baseEvent);

expect(result!.cookies).toEqual({ session: "abc", lang: "en" });
});

it("should extract route from routeKey", () => {
const result = extractHTTPDataFromEvent(baseEvent);
expect(result!.route).toBe("/my/{id}");
});

it("should not include route when routeKey is absent", () => {
const event = { ...baseEvent, routeKey: undefined };
const result = extractHTTPDataFromEvent(event);
expect(result!.route).toBeUndefined();
expect("route" in result!).toBe(false);
});

it("should not include route when routeKey produces an empty string", () => {
const event = { ...baseEvent, routeKey: "" };
const result = extractHTTPDataFromEvent(event);
expect(result!.route).toBeUndefined();
expect("route" in result!).toBe(false);
});

it("should not include cookies when cookies array is absent", () => {
const event = { ...baseEvent, cookies: undefined };
const result = extractHTTPDataFromEvent(event);
expect(result!.cookies).toBeUndefined();
expect("cookies" in result!).toBe(false);
});
});

describe("ALB", () => {
const baseEvent = {
httpMethod: "GET",
path: "/alb/path",
headers: {
host: "example.com",
"x-forwarded-for": "9.8.7.6, 10.0.0.1",
cookie: "token=xyz",
},
queryStringParameters: { key: "val" },
body: null,
isBase64Encoded: false,
requestContext: {
elb: {
targetGroupArn: "arn:aws:elasticloadbalancing:us-east-1:123456789:targetgroup/my-tg/abc",
},
},
};

it("should extract HTTP data correctly", () => {
const result = extractHTTPDataFromEvent(baseEvent);

expect(result).toBeDefined();
expect(result!.method).toBe("GET");
expect(result!.path).toBe("/alb/path");
});

it("should extract client IP from x-forwarded-for", () => {
const result = extractHTTPDataFromEvent(baseEvent);
expect(result!.clientIp).toBe("9.8.7.6");
});

it("should parse cookies from the cookie header", () => {
const result = extractHTTPDataFromEvent(baseEvent);
expect(result!.cookies).toEqual({ token: "xyz" });
expect(result!.headers.cookie).toBeUndefined();
});

it("should not have route or pathParams", () => {
const result = extractHTTPDataFromEvent(baseEvent);
expect(result!.route).toBeUndefined();
expect(result!.pathParams).toBeUndefined();
});
});

describe("Lambda Function URL", () => {
const baseEvent = {
version: "2.0",
rawPath: "/url/path",
rawQueryString: "",
headers: { host: "abc123.lambda-url.us-east-1.on.aws" },
queryStringParameters: null,
body: null,
isBase64Encoded: false,
cookies: ["token=xyz"],
requestContext: {
domainName: "abc123.lambda-url.us-east-1.on.aws",
http: {
method: "GET",
path: "/url/path",
sourceIp: "11.12.13.14",
},
},
};

it("should extract HTTP data correctly", () => {
const result = extractHTTPDataFromEvent(baseEvent);

expect(result).toBeDefined();
expect(result!.method).toBe("GET");
expect(result!.path).toBe("/url/path");
expect(result!.clientIp).toBe("11.12.13.14");
});

it("should parse cookies from the cookies array", () => {
const result = extractHTTPDataFromEvent(baseEvent);
expect(result!.cookies).toEqual({ token: "xyz" });
});

it("should not have route", () => {
const result = extractHTTPDataFromEvent(baseEvent);
expect(result!.route).toBeUndefined();
});
});
});
Loading
Loading