Skip to content
Draft
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
12 changes: 11 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,13 @@ RUN cp -r dist /nodejs/node_modules/datadog-lambda-js
RUN cp ./src/runtime/module_importer.js /nodejs/node_modules/datadog-lambda-js/runtime

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
# Copy the dependencies to the modules folder
Expand All @@ -46,6 +47,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
17 changes: 17 additions & 0 deletions scripts/move_ddtrace_dependency.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
// Moves the dd-trace dependency from devDependencies to dependencies within package.json.
// Also promotes selected dd-trace optionalDependencies to direct dependencies so they
// survive `yarn install --production=true --ignore-optional`.
// This is used when building the Layer

// USAGE: ./move_dd_trace_dependency.js "$(cat package.json)" > package.json
Expand All @@ -10,10 +12,25 @@ moveDependency('@datadog/pprof')
moveDependency('@opentelemetry/api')
moveDependency('@opentelemetry/api-logs')

addOptionalFromDdTrace('@datadog/native-appsec')

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not moveDependency('@datadog/native-appsec')?


console.log(JSON.stringify(file, null, 2));

function moveDependency (name) {
const ddTraceVersion = file.devDependencies[name];
delete file.devDependencies[name];
file.dependencies[name] = ddTraceVersion;
}

function addOptionalFromDdTrace (name) {
try {
const ddTracePkg = require('dd-trace/package.json')
const version = ddTracePkg.optionalDependencies?.[name]
if (version) {
file.dependencies[name] = version
}
} catch {
// dd-trace not installed yet; skip
}
}

240 changes: 240 additions & 0 deletions src/appsec/event-data-extractor.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
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 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}");
});
});

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