-
Notifications
You must be signed in to change notification settings - Fork 28
Add hook to monitor outbound requests #842
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
hansott
wants to merge
15
commits into
main
Choose a base branch
from
outbound-request-hook
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
2f41e4b
Add hook to monitor outbound requests
hansott 7bfd74f
Revert change to onConnectHostname
hansott 7479c59
Refactor: use generic addHook/removeHook functions
hansott 0282d36
Add hooks support for Undici
hansott b9d63e0
Fix typo
hansott 552aaef
Re-add comment
hansott 59e4df5
Add test for fetch
hansott 133337b
Fix linting errors
hansott 4858e92
Remove usage of doesNotThrow
hansott 00c39d7
Add test for http.request/get
hansott f0cd16c
Format code
hansott 91c09e3
Simplify
hansott a3613f3
Improve test coverage
hansott 25c14fa
Merge branch 'main' of github.com:AikidoSec/node-RASP into outbound-r…
hansott 3d5cb21
Fix merge
hansott File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| # Monitoring Outbound Requests | ||
|
|
||
| To monitor outbound HTTP/HTTPS requests made by your application, you can use the `addHook` function with the `beforeOutboundRequest` hook. This is useful when you want to track external API calls, log outbound traffic, or analyze what domains your application connects to. | ||
|
|
||
| ## Basic Usage | ||
|
|
||
| ```js | ||
| const { addHook } = require("@aikidosec/firewall"); | ||
|
|
||
| addHook("beforeOutboundRequest", ({ url, port, method }) => { | ||
| // url is a URL object: https://nodejs.org/api/url.html#class-url | ||
| console.log(`${new Date().toISOString()} - ${method} ${url.href}`); | ||
| }); | ||
| ``` | ||
|
|
||
| ## Removing Hooks | ||
|
|
||
| You can remove a previously registered hook using the `removeHook` function: | ||
|
|
||
| ```js | ||
| const { addHook, removeHook } = require("@aikidosec/firewall"); | ||
|
|
||
| function myHook({ url, port, method }) { | ||
| console.log(`${method} ${url.href}`); | ||
| } | ||
|
|
||
| addHook("beforeOutboundRequest", myHook); | ||
|
|
||
| // Later, when you want to remove it: | ||
| removeHook("beforeOutboundRequest", myHook); | ||
| ``` | ||
|
|
||
| ## Important Notes | ||
|
|
||
| - You can register multiple hooks by calling `addHook` multiple times. | ||
| - The same hook function can only be registered once (duplicates are automatically prevented). | ||
| - Hooks are triggered for all HTTP/HTTPS requests made through Node.js built-in modules (`http`, `https`), builtin fetch function, undici and anything that uses that. | ||
| - Hooks are called when the connection is initiated, before knowing if Zen will block the request. | ||
| - Errors thrown in hooks (both sync and async) are silently caught and not logged to prevent breaking your application. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| import * as t from "tap"; | ||
| import { | ||
| addHook, | ||
| removeHook, | ||
| executeHooks, | ||
| OutboundRequestInfo, | ||
| } from "./hooks"; | ||
|
|
||
| t.test("it works", async (t) => { | ||
| let hookOneCalls = 0; | ||
| let hookTwoCalls = 0; | ||
|
|
||
| const testRequest: OutboundRequestInfo = { | ||
| url: new URL("https://example.com"), | ||
| port: 443, | ||
| method: "GET", | ||
| }; | ||
|
|
||
| function hook1(request: OutboundRequestInfo) { | ||
| t.equal(request.url.href, "https://example.com/"); | ||
| t.equal(request.port, 443); | ||
| t.equal(request.method, "GET"); | ||
| hookOneCalls++; | ||
| } | ||
|
|
||
| function hook2(request: OutboundRequestInfo) { | ||
| t.equal(request.url.href, "https://example.com/"); | ||
| t.equal(request.port, 443); | ||
| t.equal(request.method, "GET"); | ||
| hookTwoCalls++; | ||
| } | ||
|
|
||
| function hook3() { | ||
| throw new Error("hook3 should not be called"); | ||
| } | ||
|
|
||
| t.same(hookOneCalls, 0, "hookOneCalls starts at 0"); | ||
| t.same(hookTwoCalls, 0, "hookTwoCalls starts at 0"); | ||
|
|
||
| executeHooks("beforeOutboundRequest", testRequest); | ||
|
|
||
| t.same(hookOneCalls, 0, "hookOneCalls still at 0"); | ||
| t.same(hookTwoCalls, 0, "hookTwoCalls still at 0"); | ||
|
|
||
| addHook("beforeOutboundRequest", hook1); | ||
| // @ts-expect-error some other hook is not defined in the types | ||
| addHook("someOtherHook", hook3); | ||
| executeHooks("beforeOutboundRequest", testRequest); | ||
|
|
||
| t.equal(hookOneCalls, 1, "hook1 called once"); | ||
| t.equal(hookTwoCalls, 0, "hook2 not called"); | ||
|
|
||
| addHook("beforeOutboundRequest", hook2); | ||
| executeHooks("beforeOutboundRequest", testRequest); | ||
|
|
||
| t.equal(hookOneCalls, 2, "hook1 called twice"); | ||
| t.equal(hookTwoCalls, 1, "hook2 called once"); | ||
|
|
||
| removeHook("beforeOutboundRequest", hook1); | ||
| executeHooks("beforeOutboundRequest", testRequest); | ||
|
|
||
| t.equal(hookOneCalls, 2, "hook1 still called twice"); | ||
| t.equal(hookTwoCalls, 2, "hook2 called twice"); | ||
|
|
||
| removeHook("beforeOutboundRequest", hook2); | ||
| executeHooks("beforeOutboundRequest", testRequest); | ||
|
|
||
| t.equal(hookOneCalls, 2, "hook1 still called twice"); | ||
| t.equal(hookTwoCalls, 2, "hook2 still called twice"); | ||
| }); | ||
|
|
||
| t.test("it handles errors gracefully", async (t) => { | ||
| let successCalls = 0; | ||
|
|
||
| function throwingHook() { | ||
| throw new Error("This should be caught"); | ||
| } | ||
|
|
||
| function successHook() { | ||
| successCalls++; | ||
| } | ||
|
|
||
| const testRequest: OutboundRequestInfo = { | ||
| url: new URL("https://example.com"), | ||
| port: 443, | ||
| method: "POST", | ||
| }; | ||
|
|
||
| addHook("beforeOutboundRequest", throwingHook); | ||
| addHook("beforeOutboundRequest", successHook); | ||
|
|
||
| // Should not throw even though one hook throws | ||
| executeHooks("beforeOutboundRequest", testRequest); | ||
|
|
||
| t.equal( | ||
| successCalls, | ||
| 1, | ||
| "success hook still called despite error in other hook" | ||
| ); | ||
|
|
||
| removeHook("beforeOutboundRequest", throwingHook); | ||
| removeHook("beforeOutboundRequest", successHook); | ||
| }); | ||
|
|
||
| t.test("it handles async hooks with rejected promises", async (t) => { | ||
| let asyncCalls = 0; | ||
|
|
||
| async function asyncHook() { | ||
| asyncCalls++; | ||
| throw new Error("Async error"); | ||
| } | ||
|
|
||
| const testRequest: OutboundRequestInfo = { | ||
| url: new URL("https://example.com"), | ||
| port: 443, | ||
| method: "DELETE", | ||
| }; | ||
|
|
||
| addHook("beforeOutboundRequest", asyncHook); | ||
|
|
||
| // Should not throw even though async hook rejects | ||
| executeHooks("beforeOutboundRequest", testRequest); | ||
|
|
||
| t.equal(asyncCalls, 1, "async hook was called"); | ||
|
|
||
| removeHook("beforeOutboundRequest", asyncHook); | ||
| }); | ||
|
|
||
| t.test("it prevents duplicate hooks using Set", async (t) => { | ||
| let hookCalls = 0; | ||
|
|
||
| function hook() { | ||
| hookCalls++; | ||
| } | ||
|
|
||
| const testRequest: OutboundRequestInfo = { | ||
| url: new URL("https://example.com"), | ||
| port: 443, | ||
| method: "GET", | ||
| }; | ||
|
|
||
| addHook("beforeOutboundRequest", hook); | ||
| addHook("beforeOutboundRequest", hook); // Try to add the same hook again | ||
|
|
||
| executeHooks("beforeOutboundRequest", testRequest); | ||
|
|
||
| t.equal(hookCalls, 1, "hook only called once despite being added twice"); | ||
|
|
||
| removeHook("beforeOutboundRequest", hook); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,60 @@ | ||
| export type OutboundRequestInfo = { | ||
| url: URL; | ||
| port: number; | ||
| method: string; | ||
| }; | ||
|
|
||
| type HookName = "beforeOutboundRequest"; | ||
|
|
||
| // Map hook names to argument types | ||
| interface HookTypes { | ||
| beforeOutboundRequest: { | ||
| args: [data: OutboundRequestInfo]; | ||
| }; | ||
| } | ||
|
|
||
| const hooks = new Map< | ||
| HookName, | ||
| Set<(...args: HookTypes[HookName]["args"]) => void | Promise<void>> | ||
| >(); | ||
|
|
||
| export function addHook<N extends HookName>( | ||
| name: N, | ||
| fn: (...args: HookTypes[N]["args"]) => void | Promise<void> | ||
| ) { | ||
| if (!hooks.has(name)) { | ||
| hooks.set(name, new Set([fn])); | ||
| } else { | ||
| hooks.get(name)!.add(fn); | ||
| } | ||
| } | ||
|
|
||
| export function removeHook<N extends HookName>( | ||
| name: N, | ||
| fn: (...args: HookTypes[N]["args"]) => void | Promise<void> | ||
| ) { | ||
| hooks.get(name)?.delete(fn); | ||
| } | ||
|
|
||
| export function executeHooks<N extends HookName>( | ||
| name: N, | ||
| ...args: [...HookTypes[N]["args"]] | ||
| ): void { | ||
| const hookSet = hooks.get(name); | ||
|
|
||
| for (const fn of hookSet ?? []) { | ||
| try { | ||
| const result = ( | ||
| fn as (...args: HookTypes[N]["args"]) => void | Promise<void> | ||
| )(...args); | ||
| // If it returns a promise, catch any errors but don't wait | ||
| if (result instanceof Promise) { | ||
| result.catch(() => { | ||
| // Silently ignore errors from user hooks | ||
| }); | ||
| } | ||
| } catch { | ||
| // Silently ignore errors from user hooks | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.