Skip to content

Commit

Permalink
Merge branch 'main' into primsa
Browse files Browse the repository at this point in the history
  • Loading branch information
timokoessler committed Dec 13, 2024
2 parents 31be3d0 + 7c3c217 commit 5fbf45d
Show file tree
Hide file tree
Showing 29 changed files with 4,252 additions and 274 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,14 @@ $ yarn add --exact @aikidosec/firewall

For framework- and provider- specific instructions, check out our docs:

- [Express.js-based apps](docs/express.md)
- [Express](docs/express.md)
- [Fastify](docs/fastify.md)
- [Hapi](docs/hapi.md)
- [Koa](docs/koa.md)
- [Hono](docs/hono.md)
- [NestJS](docs/nestjs.md)
- [micro](docs/micro.md)
- [Next.js](docs/next.md)
- [AWS Lambda](docs/lambda.md)
- [Google Cloud Functions](docs/cloud-functions.md)
- [Google Cloud Pub/Sub](docs/pubsub.md)
Expand Down
55 changes: 55 additions & 0 deletions end2end/tests/node-red.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
const t = require("tap");
const { spawn } = require("child_process");
const { resolve } = require("path");
const timeout = require("../timeout");

const pathToApp = resolve(__dirname, "../../sample-apps/node-red");

t.test("it serves debug script", (t) => {
const server = spawn(`node_modules/.bin/node-red`, {
env: {
...process.env,
AIKIDO_DEBUG: "true",
AIKIDO_BLOCK: "true",
NODE_OPTIONS: "-r @aikidosec/firewall",
},
cwd: pathToApp,
});

server.on("close", () => {
t.end();
});

server.on("error", (err) => {
t.fail(err);
});

let stdout = "";
server.stdout.on("data", (data) => {
stdout += data.toString();
});

let stderr = "";
server.stderr.on("data", (data) => {
stderr += data.toString();
});

// Wait for the server to start
timeout(5000)
.then(() => {
return Promise.all([
fetch(`http://127.0.0.1:1880/debug/view/debug-utils.js`, {
signal: AbortSignal.timeout(5000),
}),
]);
})
.then(([script]) => {
t.equal(script.status, 200);
})
.catch((error) => {
t.fail(error);
})
.finally(() => {
server.kill();
});
});
7 changes: 7 additions & 0 deletions library/agent/Agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -746,10 +746,17 @@ t.test("it sends hostnames and routes along with heartbeat", async () => {
{
hostname: "aikido.dev",
port: 443,
hits: 1,
},
{
hostname: "aikido.dev",
port: 80,
hits: 1,
},
{
hostname: "google.com",
port: 443,
hits: 1,
},
],
routes: [
Expand Down
94 changes: 83 additions & 11 deletions library/agent/Hostnames.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,104 @@ t.test("it works", async () => {
const hostnames = new Hostnames(3);
t.same(hostnames.asArray(), []);

hostnames.add("aikido.dev", 433);
t.same(hostnames.asArray(), [{ hostname: "aikido.dev", port: 433 }]);
hostnames.add("aikido.dev", 443);
t.same(hostnames.asArray(), [{ hostname: "aikido.dev", port: 443, hits: 1 }]);

hostnames.add("aikido.dev", 80);
t.same(hostnames.asArray(), [{ hostname: "aikido.dev", port: 433 }]);
t.same(hostnames.asArray(), [
{ hostname: "aikido.dev", port: 443, hits: 1 },
{ hostname: "aikido.dev", port: 80, hits: 1 },
]);

hostnames.add("google.com", 80);
t.same(hostnames.asArray(), [
{ hostname: "aikido.dev", port: 433 },
{ hostname: "google.com", port: 80 },
{ hostname: "aikido.dev", port: 443, hits: 1 },
{ hostname: "aikido.dev", port: 80, hits: 1 },
{ hostname: "google.com", port: 80, hits: 1 },
]);

hostnames.add("google.com", undefined);
t.same(hostnames.asArray(), [
{ hostname: "aikido.dev", port: 80, hits: 1 },
{ hostname: "google.com", port: 80, hits: 1 },
{ hostname: "google.com", port: undefined, hits: 1 },
]);

hostnames.add("github.com", 80);
t.same(hostnames.asArray(), [
{ hostname: "aikido.dev", port: 433 },
{ hostname: "google.com", port: 80 },
{ hostname: "github.com", port: 80 },
{ hostname: "google.com", port: 80, hits: 1 },
{ hostname: "google.com", port: undefined, hits: 1 },
{ hostname: "github.com", port: 80, hits: 1 },
]);

hostnames.add("jetbrains.com", 80);
t.same(hostnames.asArray(), [
{ hostname: "google.com", port: 80 },
{ hostname: "github.com", port: 80 },
{ hostname: "jetbrains.com", port: 80 },
{ hostname: "google.com", port: undefined, hits: 1 },
{ hostname: "github.com", port: 80, hits: 1 },
{ hostname: "jetbrains.com", port: 80, hits: 1 },
]);

hostnames.clear();
t.same(hostnames.asArray(), []);
});

t.test("it respects max size", async () => {
const hostnames = new Hostnames(2);
hostnames.add("aikido.dev", 1);
hostnames.add("aikido.dev", 2);

t.same(hostnames.asArray(), [
{ hostname: "aikido.dev", port: 1, hits: 1 },
{ hostname: "aikido.dev", port: 2, hits: 1 },
]);

hostnames.add("aikido.dev", 3);
hostnames.add("aikido.dev", 4);

t.same(hostnames.asArray(), [
{ hostname: "aikido.dev", port: 3, hits: 1 },
{ hostname: "aikido.dev", port: 4, hits: 1 },
]);

hostnames.add("google.com", 1);

t.same(hostnames.asArray(), [
{ hostname: "aikido.dev", port: 4, hits: 1 },
{ hostname: "google.com", port: 1, hits: 1 },
]);

hostnames.add("google.com", 2);

t.same(hostnames.asArray(), [
{ hostname: "google.com", port: 1, hits: 1 },
{ hostname: "google.com", port: 2, hits: 1 },
]);
});

t.test("it tracks hits", async () => {
const hostnames = new Hostnames(3);

hostnames.add("aikido.dev", 443);
hostnames.add("aikido.dev", 443);
t.same(hostnames.asArray(), [{ hostname: "aikido.dev", port: 443, hits: 2 }]);

hostnames.add("aikido.dev", 80);
t.same(hostnames.asArray(), [
{ hostname: "aikido.dev", port: 443, hits: 2 },
{ hostname: "aikido.dev", port: 80, hits: 1 },
]);

hostnames.add("google.com", 80);
t.same(hostnames.asArray(), [
{ hostname: "aikido.dev", port: 443, hits: 2 },
{ hostname: "aikido.dev", port: 80, hits: 1 },
{ hostname: "google.com", port: 80, hits: 1 },
]);

hostnames.add("aikido.dev", 443);
t.same(hostnames.asArray(), [
{ hostname: "aikido.dev", port: 443, hits: 3 },
{ hostname: "aikido.dev", port: 80, hits: 1 },
{ hostname: "google.com", port: 80, hits: 1 },
]);
});
52 changes: 39 additions & 13 deletions library/agent/Hostnames.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,56 @@
type Ports = Map<number, number>;

export class Hostnames {
private map: Map<string, number | undefined> = new Map();
private map: Map<string, Ports> = new Map();

constructor(private readonly maxEntries: number = 200) {}

add(hostname: string, port: number | undefined) {
if (this.map.has(hostname)) {
return;
add(hostname: string, port: number | undefined = -1) {
if (!this.map.has(hostname)) {
this.map.set(hostname, new Map([[port, 1]]));
} else {
const ports = this.map.get(hostname) as Ports;
if (!ports.has(port)) {
ports.set(port, 1);
} else {
ports.set(port, ports.get(port)! + 1);
}
}

if (this.map.size >= this.maxEntries) {
if (this.length > this.maxEntries) {
const firstAdded = this.map.keys().next().value;
if (firstAdded) {
this.map.delete(firstAdded);
const ports: Ports = this.map.get(firstAdded) as Ports;

if (ports.size > 1) {
const firstPort = ports.keys().next().value;
if (firstPort) {
ports.delete(firstPort);
}
} else {
this.map.delete(firstAdded);
}
}
}
}

this.map.set(hostname, port);
get length() {
return Array.from(this.map.values()).reduce(
(total: number, ports: Ports) => total + ports.size,
0
);
}

asArray() {
return Array.from(this.map.entries()).map(([hostname, port]) => {
return {
hostname,
port,
};
});
return Array.from(this.map.entries()).flatMap(([hostname, ports]) =>
Array.from(ports.entries()).map(([port, hits]) => {
return {
hostname,
port: port === -1 ? undefined : port,
hits,
};
})
);
}

clear() {
Expand Down
16 changes: 13 additions & 3 deletions library/helpers/wrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,19 @@ export function createWrappedFunction(
// .inspect("realpath", (args) => {...})
// We don't want to lose the original function's properties.
// Most of the functions we're wrapping don't have any properties, so this is a rare case.
for (const prop in original) {
if (original.hasOwnProperty(prop)) {
defineProperty(wrapped, prop, original[prop as keyof Function]);
// Inspired by https://github.com/DataDog/dd-trace-js/blob/master/packages/datadog-shimmer/src/shimmer.js#L8

Object.setPrototypeOf(wrapped, original);

const props = Object.getOwnPropertyDescriptors(original);
const keys = Reflect.ownKeys(props);

for (const key of keys) {
try {
// Define the property on the wrapped function, keeping the original property's attributes.
Object.defineProperty(wrapped, key as any, props[key as any]);
} catch (e) {
//
}
}

Expand Down
8 changes: 6 additions & 2 deletions library/middleware/hono.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
import { shouldBlockRequest } from "./shouldBlockRequest";
import type { Hono } from "hono";
import type { Env, Hono, Schema } from "hono";
import { escapeHTML } from "../helpers/escapeHTML";

/**
* Calling this function will setup rate limiting and user blocking for the provided Hono app.
* Attacks will still be blocked by Zen if you do not call this function.
* Execute this function as early as possible in your Hono app, but after the middleware that sets the user.
*/
export function addHonoMiddleware(app: Hono) {
export function addHonoMiddleware<
E extends Env,
S extends Schema,
BasePath extends string,
>(app: Hono<E, S, BasePath>) {
app.use(async (c, next) => {
const result = shouldBlockRequest();

Expand Down
12 changes: 6 additions & 6 deletions library/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 11 additions & 2 deletions library/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,16 @@
"name": "@aikidosec/firewall",
"version": "0.0.0",
"description": "Zen by Aikido is an embedded Web Application Firewall that autonomously protects Node.js apps against common and critical attacks",
"repository": "https://github.com/AikidoSec/firewall-node",
"repository": {
"type": "git",
"url": "git+https://github.com/AikidoSec/firewall-node.git"
},
"type": "commonjs",
"homepage": "https://aikido.dev/zen",
"author": "Aikido Security",
"bugs": {
"url": "https://github.com/AikidoSec/firewall-node/issues"
},
"keywords": [
"security",
"nosql",
Expand Down Expand Up @@ -86,7 +95,7 @@
"mongodb": "~6.9",
"mongodb-v4": "npm:mongodb@^4.0.0",
"mongodb-v5": "npm:mongodb@^5.0.0",
"mongodb-v6": "npm:mongodb@~6.9",
"mongodb-v6": "npm:mongodb@^6.0.0",
"mysql": "^2.18.1",
"mysql2": "^3.10.0",
"needle": "^3.3.1",
Expand Down
4 changes: 2 additions & 2 deletions library/sinks/Fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,14 +88,14 @@ t.test(
await fetch("http://app.aikido.dev");

t.same(agent.getHostnames().asArray(), [
{ hostname: "app.aikido.dev", port: 80 },
{ hostname: "app.aikido.dev", port: 80, hits: 1 },
]);
agent.getHostnames().clear();

await fetch(new URL("https://app.aikido.dev"));

t.same(agent.getHostnames().asArray(), [
{ hostname: "app.aikido.dev", port: 443 },
{ hostname: "app.aikido.dev", port: 443, hits: 1 },
]);
agent.getHostnames().clear();

Expand Down
2 changes: 1 addition & 1 deletion library/sinks/HTTPRequest.axios.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ t.test("it works", { skip: "SSRF redirect check disabled atm" }, async (t) => {
});

t.same(agent.getHostnames().asArray(), [
{ hostname: "www.aikido.dev", port: 443 },
{ hostname: "www.aikido.dev", port: 443, hits: 1 },
]);
agent.getHostnames().clear();

Expand Down
Loading

0 comments on commit 5fbf45d

Please sign in to comment.