Skip to content

Commit 9d85c50

Browse files
authored
Merge pull request #843 from AikidoSec/fix-esm-missing-outbound
Fix missing outbound hostname
2 parents adf7b5e + 8236c6c commit 9d85c50

File tree

3 files changed

+182
-0
lines changed

3 files changed

+182
-0
lines changed
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
import { spawn } from "child_process";
2+
import { resolve } from "path";
3+
import { test } from "node:test";
4+
import { equal, fail, partialDeepStrictEqual } from "node:assert";
5+
import { getRandomPort } from "./utils/get-port.mjs";
6+
import { timeout } from "./utils/timeout.mjs";
7+
8+
const pathToAppDir = resolve(
9+
import.meta.dirname,
10+
"../../sample-apps/hono-pg-ts-esm"
11+
);
12+
13+
const port = await getRandomPort();
14+
15+
const testServerUrl = "http://localhost:5874";
16+
17+
test("It reports own http requests in heartbeat events", async () => {
18+
const response = await fetch(`${testServerUrl}/api/runtime/apps`, {
19+
method: "POST",
20+
});
21+
const body = await response.json();
22+
const token = body.token;
23+
24+
const server = spawn(
25+
`node`,
26+
[
27+
"--require",
28+
"@aikidosec/firewall/instrument",
29+
"--experimental-strip-types",
30+
"./app.ts",
31+
port,
32+
],
33+
{
34+
cwd: pathToAppDir,
35+
env: {
36+
...process.env,
37+
AIKIDO_TOKEN: token,
38+
AIKIDO_ENDPOINT: testServerUrl,
39+
AIKIDO_REALTIME_ENDPOINT: testServerUrl,
40+
AIKIDO_DEBUG: "true",
41+
AIKIDO_BLOCK: "true",
42+
},
43+
}
44+
);
45+
46+
try {
47+
server.on("error", (err) => {
48+
fail(err);
49+
});
50+
51+
let stdout = "";
52+
server.stdout.on("data", (data) => {
53+
stdout += data.toString();
54+
});
55+
56+
let stderr = "";
57+
server.stderr.on("data", (data) => {
58+
stderr += data.toString();
59+
});
60+
61+
// Wait for the server to start
62+
await timeout(2000);
63+
64+
await fetch(`http://127.0.0.1:${port}/`, {
65+
method: "GET",
66+
signal: AbortSignal.timeout(5000),
67+
});
68+
69+
// Wait for first heartbeat to be sent
70+
await timeout(31000);
71+
72+
const eventsResponse = await fetch(`${testServerUrl}/api/runtime/events`, {
73+
method: "GET",
74+
headers: {
75+
Authorization: token,
76+
},
77+
signal: AbortSignal.timeout(5000),
78+
});
79+
80+
const events = await eventsResponse.json();
81+
const heartbeatEvents = events.filter(
82+
(event) => event.type === "heartbeat"
83+
);
84+
85+
equal(heartbeatEvents.length, 1);
86+
partialDeepStrictEqual(heartbeatEvents, [
87+
{
88+
type: "heartbeat",
89+
hostnames: [
90+
{
91+
hostname: "localhost",
92+
port: 5874,
93+
hits: 2,
94+
},
95+
],
96+
agent: {
97+
dryMode: false,
98+
library: "firewall-node",
99+
preventedPrototypePollution: false,
100+
serverless: false,
101+
},
102+
packages: [
103+
{
104+
name: "@aikidosec/firewall",
105+
},
106+
{
107+
name: "hono",
108+
},
109+
{
110+
name: "pg",
111+
},
112+
],
113+
stats: {
114+
operations: {
115+
"pg.query": {
116+
attacksDetected: {
117+
blocked: 0,
118+
total: 0,
119+
},
120+
kind: "sql_op",
121+
},
122+
},
123+
requests: {
124+
aborted: 0,
125+
attackWaves: {
126+
blocked: 0,
127+
total: 0,
128+
},
129+
attacksDetected: {
130+
blocked: 0,
131+
total: 0,
132+
},
133+
rateLimited: 0,
134+
total: 1,
135+
},
136+
sqlTokenizationFailures: 0,
137+
},
138+
routes: [
139+
{
140+
hits: 1,
141+
method: "GET",
142+
path: "/",
143+
rateLimitedCount: 0,
144+
},
145+
],
146+
users: [],
147+
},
148+
]);
149+
} catch (err) {
150+
fail(err);
151+
} finally {
152+
server.kill();
153+
}
154+
});

end2end/tests-new/hono-pg-esm.test.mjs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ test("if bypass IP is set, attack waves are ignored for that IP", async () => {
266266
...process.env,
267267
AIKIDO_TOKEN: token,
268268
AIKIDO_ENDPOINT: testServerUrl,
269+
AIKIDO_REALTIME_ENDPOINT: testServerUrl,
269270
AIKIDO_DEBUG: "true",
270271
},
271272
}

library/helpers/fetch.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { request as requestHttp, type Agent } from "http";
22
import { request as requestHttps } from "https";
33
import { type Readable } from "stream";
44
import { createGunzip } from "zlib";
5+
import { getInstance } from "../agent/AgentSingleton";
6+
import { getPortFromURL } from "./getPortFromURL";
57

68
function request({
79
url,
@@ -20,6 +22,8 @@ function request({
2022
}): Promise<{ body: string; statusCode: number }> {
2123
const request = url.protocol === "https:" ? requestHttps : requestHttp;
2224

25+
trackRequest(url);
26+
2327
return new Promise((resolve, reject) => {
2428
// Convert URL object to string for compatibility with old https-proxy-agent versions
2529
// Old agent-base library (used by https-proxy-agent) only works with string URLs
@@ -108,3 +112,26 @@ export async function fetch({
108112
}),
109113
]);
110114
}
115+
116+
function trackRequest(url: URL) {
117+
const agent = getInstance();
118+
if (!agent) {
119+
// This should not happen
120+
return;
121+
}
122+
123+
// If the old (non ESM) hook system is used, the fetch function used
124+
// here is already a patched version that tracks requests.
125+
// If the new hook system is used, the import is executed before
126+
// the hooks are applied, so we need to track the request here.
127+
if (!agent.isUsingNewInstrumentation()) {
128+
return;
129+
}
130+
131+
const port = getPortFromURL(url);
132+
if (!port) {
133+
return;
134+
}
135+
136+
agent.onConnectHostname(url.hostname, port);
137+
}

0 commit comments

Comments
 (0)