diff --git a/library/.eslintrc.js b/library/.eslintrc.js index 7200dc02c..c68c26768 100644 --- a/library/.eslintrc.js +++ b/library/.eslintrc.js @@ -35,6 +35,7 @@ module.exports = { "max-lines-per-function": ["error", { max: 50, skipBlankLines: true }], "func-names": ["error", "as-needed"], camelcase: "error", + "max-classes-per-file": ["error", 1], }, overrides: [ { diff --git a/library/src/agent/API.ts b/library/src/agent/API.ts deleted file mode 100644 index 43d1722ad..000000000 --- a/library/src/agent/API.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { Source } from "./Source"; -import { request as requestHttp } from "node:http"; -import { request as requestHttps } from "node:https"; - -export class Token { - constructor(private readonly token: string) { - if (!this.token) { - throw new Error("Token cannot be empty"); - } - } - - toString() { - throw new Error("Please use asString() instead"); - } - - asString() { - return this.token; - } -} - -export type AgentInfo = { - dryMode: boolean; - hostname: string; - version: string; - packages: Record; - ipAddress: string; - preventedPrototypePollution: boolean; - os: { - name: string; - version: string; - }; - nodeEnv: string; - serverless: boolean; -}; - -type Started = { - type: "started"; - agent: AgentInfo; - time: number; -}; - -export type Kind = "nosql_injection" | "sql_injection"; - -type DetectedAttack = { - type: "detected_attack"; - request: { - method: string | undefined; - ipAddress: string | undefined; - userAgent: string | undefined; - url: string | undefined; - }; - attack: { - kind: Kind; - module: string; - blocked: boolean; - source: Source; - path: string; - stack: string; - metadata: Record; - }; - agent: AgentInfo; - time: number; -}; - -type ModuleName = string; - -export type Stats = Record< - ModuleName, - { - blocked: number; - allowed: number; - withoutContext: number; - total: number; - } ->; - -type Heartbeat = { - type: "heartbeat"; - stats: Stats; - agent: AgentInfo; - time: number; -}; - -export type Event = Started | DetectedAttack | Heartbeat; - -export interface API { - report(token: Token, event: Event): Promise; -} - -type ThrottleOptions = { maxEventsPerInterval: number; intervalInMs: number }; - -export class APIThrottled implements API { - private readonly maxEventsPerInterval: number; - private readonly intervalInMs: number; - private events: Event[] = []; - - constructor( - private readonly api: API, - { maxEventsPerInterval, intervalInMs }: ThrottleOptions - ) { - this.maxEventsPerInterval = maxEventsPerInterval; - this.intervalInMs = intervalInMs; - } - - async report(token: Token, event: Event) { - if (event.type === "detected_attack") { - const currentTime = Date.now(); - - this.events = this.events.filter( - (e) => e.time > currentTime - this.intervalInMs - ); - - if (this.events.length >= this.maxEventsPerInterval) { - return; - } - - this.events.push(event); - } - - await this.api.report(token, event); - } -} - -export class APIFetch implements API { - constructor( - private readonly reportingUrl: URL, - private readonly timeoutInMS: number = 5000 - ) {} - - private async fetch( - url: string, - { - signal, - method, - body, - headers, - }: { - signal: AbortSignal; - method: string; - headers: Record; - body: string; - } - ) { - const request = url.startsWith("https://") ? requestHttps : requestHttp; - - return new Promise((resolve) => { - const req = request( - url, - { - method, - headers, - signal, - }, - (res) => { - res.on("data", () => {}); - res.on("end", () => { - resolve(); - }); - } - ); - - req.on("error", () => { - resolve(); - }); - - req.write(body); - req.end(); - }); - } - - async report(token: Token, event: Event) { - const abort = new AbortController(); - await Promise.race([ - this.fetch(this.reportingUrl.toString(), { - signal: abort.signal, - method: "POST", - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${token.asString()}`, - }, - body: JSON.stringify(event), - }), - new Promise((resolve) => - setTimeout(() => { - abort.abort(); - resolve(); - }, this.timeoutInMS) - ), - ]); - } -} - -export class APIForTesting implements API { - private readonly events: Event[] = []; - - async report(token: Token, event: Event) { - this.events.push(event); - } - - getEvents() { - return this.events; - } -} diff --git a/library/src/agent/Agent.test.ts b/library/src/agent/Agent.test.ts index 1f033513d..abff8aeb2 100644 --- a/library/src/agent/Agent.test.ts +++ b/library/src/agent/Agent.test.ts @@ -2,8 +2,9 @@ import { hostname, platform, release } from "node:os"; import * as t from "tap"; import { ip } from "../helpers/ipAddress"; import { Agent } from "./Agent"; -import { APIForTesting, Token } from "./API"; -import { LoggerNoop } from "./Logger"; +import { APIForTesting } from "./api/APIForTesting"; +import { Token } from "./api/Token"; +import { LoggerNoop } from "./logger/LoggerNoop"; t.test("it sends started event", async (t) => { const logger = new LoggerNoop(); diff --git a/library/src/agent/Agent.ts b/library/src/agent/Agent.ts index f1d04db2e..1645ecbbb 100644 --- a/library/src/agent/Agent.ts +++ b/library/src/agent/Agent.ts @@ -1,9 +1,11 @@ import { hostname, platform, release } from "node:os"; import { ip } from "../helpers/ipAddress"; -import { API, AgentInfo, Token, Stats, Kind } from "./API"; -import { Logger } from "./Logger"; +import { API } from "./api/API"; +import { AgentInfo, Kind, Stats } from "./api/Event"; +import { Token } from "./api/Token"; import { Context } from "./Context"; import { resolve } from "path"; +import { Logger } from "./logger/Logger"; import { Source } from "./Source"; export class Agent { diff --git a/library/src/agent/Wrapper.ts b/library/src/agent/Wrapper.ts index edf4fd537..a0c3dc072 100644 --- a/library/src/agent/Wrapper.ts +++ b/library/src/agent/Wrapper.ts @@ -1,147 +1,4 @@ -type Interceptor = (args: unknown[], subject: unknown) => void; - -export class MethodInterceptor { - constructor( - private readonly name: string, - private readonly interceptor: Interceptor - ) { - if (!this.name) { - throw new Error("Method name is required"); - } - } - - getName() { - return this.name; - } - - getInterceptor() { - return this.interceptor; - } -} - -type ModifyingArgumentsInterceptor = ( - args: unknown[], - subject: unknown -) => unknown[]; - -export class ModifyingArgumentsMethodInterceptor { - constructor( - private readonly name: string, - private readonly interceptor: ModifyingArgumentsInterceptor - ) { - if (!this.name) { - throw new Error("Method name is required"); - } - } - - getName() { - return this.name; - } - - getInterceptor() { - return this.interceptor; - } -} - -class Selector { - private methods: (MethodInterceptor | ModifyingArgumentsMethodInterceptor)[] = - []; - - constructor(private readonly selector: (exports: unknown) => unknown) {} - - inspect(methodName: string, interceptor: Interceptor) { - const method = new MethodInterceptor(methodName, interceptor); - this.methods.push(method); - - return method; - } - - modifyArguments( - methodName: string, - interceptor: ModifyingArgumentsInterceptor - ) { - const method = new ModifyingArgumentsMethodInterceptor( - methodName, - interceptor - ); - this.methods.push(method); - - return method; - } - - getSelector() { - return this.selector; - } - - getMethodInterceptors() { - return this.methods; - } -} - -class VersionedPackage { - private selectors: Selector[] = []; - - constructor(private readonly range: string) { - if (!this.range) { - throw new Error("Version range is required"); - } - } - - getRange() { - return this.range; - } - - subject(selector: (exports: any) => unknown): Selector { - const fn = new Selector(selector); - this.selectors.push(fn); - - return fn; - } - - getSelectors() { - return this.selectors; - } -} - -class Package { - private versions: VersionedPackage[] = []; - - constructor(private readonly packageName: string) {} - - getName() { - return this.packageName; - } - - withVersion(range: string): VersionedPackage { - const pkg = new VersionedPackage(range); - this.versions.push(pkg); - - return pkg; - } - - getVersions() { - return this.versions; - } -} - -export class Hooks { - private readonly packages: Package[] = []; - - package(packageName: string): Package { - if (!packageName) { - throw new Error("Package name is required"); - } - - const pkg = new Package(packageName); - this.packages.push(pkg); - - return pkg; - } - - getPackages() { - return this.packages; - } -} +import { Hooks } from "./hooks/Hooks"; export interface Wrapper { wrap(hooks: Hooks): void; diff --git a/library/src/agent/api/API.ts b/library/src/agent/api/API.ts new file mode 100644 index 000000000..74e6e03e6 --- /dev/null +++ b/library/src/agent/api/API.ts @@ -0,0 +1,6 @@ +import { Event } from "./Event"; +import { Token } from "./Token"; + +export interface API { + report(token: Token, event: Event): Promise; +} diff --git a/library/src/agent/api/APIFetch.test.ts b/library/src/agent/api/APIFetch.test.ts new file mode 100644 index 000000000..100f3a991 --- /dev/null +++ b/library/src/agent/api/APIFetch.test.ts @@ -0,0 +1,89 @@ +import { json } from "express"; +import * as asyncHandler from "express-async-handler"; +import * as t from "tap"; +import { APIFetch } from "./APIFetch"; +import { Event } from "./Event"; +import { Token } from "./Token"; +import express = require("express"); + +function generateStartedEvent(): Event { + return { + type: "started", + time: Date.now(), + agent: { + version: "1.0.0", + dryMode: false, + hostname: "hostname", + packages: {}, + ipAddress: "ipAddress", + preventedPrototypePollution: false, + nodeEnv: "", + os: { + name: "os", + version: "version", + }, + serverless: false, + }, + }; +} + +type SeenPayload = { token: string; body: unknown }; +type StopServer = () => Promise; + +function createTestEndpoint(sleepInMs?: number): StopServer { + const seen: SeenPayload[] = []; + + const app = express(); + app.use(json()); + app.post( + "*", + asyncHandler(async (req, res) => { + if (sleepInMs) { + await new Promise((resolve) => setTimeout(resolve, sleepInMs)); + } + + seen.push({ + token: req.header("Authorization") || "", + body: req.body, + }); + + res.send({ success: true }); + }) + ); + + const server = app.listen(3000); + + return () => { + return new Promise((resolve) => + server.close(() => { + resolve(seen); + }) + ); + }; +} + +t.test("it reports event to API endpoint", async () => { + const stop = createTestEndpoint(); + const api = new APIFetch(new URL("http://localhost:3000"), 1000); + await api.report(new Token("123"), generateStartedEvent()); + const seen = await stop(); + t.same(seen.length, 1); + t.same(seen[0].token, "Bearer 123"); + // @ts-expect-error Type is not known + t.same(seen[0].body.type, "started"); +}); + +t.test("it respects timeout", async () => { + const stop = createTestEndpoint(2000); + const api = new APIFetch(new URL("http://localhost:3000"), 1000); + const start = performance.now(); + await api.report(new Token("123"), generateStartedEvent()); + const finish = performance.now(); + // Added 200ms to prevent flakiness + t.same(finish - start < 1200, true); + await stop(); +}); + +t.test("it throws error if token is empty", async () => { + t.throws(() => new Token("")); +}); diff --git a/library/src/agent/api/APIFetch.ts b/library/src/agent/api/APIFetch.ts new file mode 100644 index 000000000..d2720183e --- /dev/null +++ b/library/src/agent/api/APIFetch.ts @@ -0,0 +1,74 @@ +import { request as requestHttp } from "node:http"; +import { request as requestHttps } from "node:https"; +import { API } from "./API"; +import { Token } from "./Token"; +import { Event } from "./Event"; + +export class APIFetch implements API { + constructor( + private readonly reportingUrl: URL, + private readonly timeoutInMS: number = 5000 + ) {} + + private async fetch( + url: string, + { + signal, + method, + body, + headers, + }: { + signal: AbortSignal; + method: string; + headers: Record; + body: string; + } + ) { + const request = url.startsWith("https://") ? requestHttps : requestHttp; + + return new Promise((resolve) => { + const req = request( + url, + { + method, + headers, + signal, + }, + (res) => { + res.on("data", () => {}); + res.on("end", () => { + resolve(); + }); + } + ); + + req.on("error", () => { + resolve(); + }); + + req.write(body); + req.end(); + }); + } + + async report(token: Token, event: Event) { + const abort = new AbortController(); + await Promise.race([ + this.fetch(this.reportingUrl.toString(), { + signal: abort.signal, + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token.asString()}`, + }, + body: JSON.stringify(event), + }), + new Promise((resolve) => + setTimeout(() => { + abort.abort(); + resolve(); + }, this.timeoutInMS) + ), + ]); + } +} diff --git a/library/src/agent/api/APIForTesting.ts b/library/src/agent/api/APIForTesting.ts new file mode 100644 index 000000000..22484dd2b --- /dev/null +++ b/library/src/agent/api/APIForTesting.ts @@ -0,0 +1,15 @@ +import { Token } from "./Token"; +import { Event } from "./Event"; +import { API } from "./API"; + +export class APIForTesting implements API { + private readonly events: Event[] = []; + + async report(token: Token, event: Event) { + this.events.push(event); + } + + getEvents() { + return this.events; + } +} diff --git a/library/src/agent/API.test.ts b/library/src/agent/api/APIThrottled.test.ts similarity index 70% rename from library/src/agent/API.test.ts rename to library/src/agent/api/APIThrottled.test.ts index 24a07c862..23cd80c41 100644 --- a/library/src/agent/API.test.ts +++ b/library/src/agent/api/APIThrottled.test.ts @@ -1,8 +1,9 @@ import * as t from "tap"; -import { APIForTesting, APIThrottled, Token, Event, APIFetch } from "./API"; -import express = require("express"); -import { json } from "express"; -import * as asyncHandler from "express-async-handler"; + +import { APIForTesting } from "./APIForTesting"; +import { APIThrottled } from "./APIThrottled"; +import { Token } from "./Token"; +import { Event } from "./Event"; function generateAttackEvent(): Event { return { @@ -158,64 +159,3 @@ t.test("it always allows heartbeat events", async () => { await throttled.report(token, generateHeartbeatEvent()); t.same(api.getEvents().length, 6); }); - -type SeenPayload = { token: string; body: unknown }; -type StopServer = () => Promise; - -function createTestEndpoint(sleepInMs?: number): StopServer { - const seen: SeenPayload[] = []; - - const app = express(); - app.use(json()); - app.post( - "*", - asyncHandler(async (req, res) => { - if (sleepInMs) { - await new Promise((resolve) => setTimeout(resolve, sleepInMs)); - } - - seen.push({ - token: req.header("Authorization") || "", - body: req.body, - }); - - res.send({ success: true }); - }) - ); - - const server = app.listen(3000); - - return () => { - return new Promise((resolve) => - server.close(() => { - resolve(seen); - }) - ); - }; -} - -t.test("it reports event to API endpoint", async () => { - const stop = createTestEndpoint(); - const api = new APIFetch(new URL("http://localhost:3000"), 1000); - await api.report(new Token("123"), generateStartedEvent()); - const seen = await stop(); - t.same(seen.length, 1); - t.same(seen[0].token, "Bearer 123"); - // @ts-expect-error Type is not known - t.same(seen[0].body.type, "started"); -}); - -t.test("it respects timeout", async () => { - const stop = createTestEndpoint(2000); - const api = new APIFetch(new URL("http://localhost:3000"), 1000); - const start = performance.now(); - await api.report(new Token("123"), generateStartedEvent()); - const finish = performance.now(); - // Added 200ms to prevent flakiness - t.same(finish - start < 1200, true); - await stop(); -}); - -t.test("it throws error if token is empty", async () => { - t.throws(() => new Token("")); -}); diff --git a/library/src/agent/api/APIThrottled.ts b/library/src/agent/api/APIThrottled.ts new file mode 100644 index 000000000..c08dfc591 --- /dev/null +++ b/library/src/agent/api/APIThrottled.ts @@ -0,0 +1,37 @@ +import { API } from "./API"; +import { Token } from "./Token"; +import { Event } from "./Event"; + +type ThrottleOptions = { maxEventsPerInterval: number; intervalInMs: number }; + +export class APIThrottled implements API { + private readonly maxEventsPerInterval: number; + private readonly intervalInMs: number; + private events: Event[] = []; + + constructor( + private readonly api: API, + { maxEventsPerInterval, intervalInMs }: ThrottleOptions + ) { + this.maxEventsPerInterval = maxEventsPerInterval; + this.intervalInMs = intervalInMs; + } + + async report(token: Token, event: Event) { + if (event.type === "detected_attack") { + const currentTime = Date.now(); + + this.events = this.events.filter( + (e) => e.time > currentTime - this.intervalInMs + ); + + if (this.events.length >= this.maxEventsPerInterval) { + return; + } + + this.events.push(event); + } + + await this.api.report(token, event); + } +} diff --git a/library/src/agent/api/Event.ts b/library/src/agent/api/Event.ts new file mode 100644 index 000000000..26293e48c --- /dev/null +++ b/library/src/agent/api/Event.ts @@ -0,0 +1,66 @@ +import { Source } from "../Source"; + +export type AgentInfo = { + dryMode: boolean; + hostname: string; + version: string; + packages: Record; + ipAddress: string; + preventedPrototypePollution: boolean; + os: { + name: string; + version: string; + }; + nodeEnv: string; + serverless: boolean; +}; + +type Started = { + type: "started"; + agent: AgentInfo; + time: number; +}; + +export type Kind = "nosql_injection" | "sql_injection"; + +type DetectedAttack = { + type: "detected_attack"; + request: { + method: string | undefined; + ipAddress: string | undefined; + userAgent: string | undefined; + url: string | undefined; + }; + attack: { + kind: Kind; + module: string; + blocked: boolean; + source: Source; + path: string; + stack: string; + metadata: Record; + }; + agent: AgentInfo; + time: number; +}; + +type ModuleName = string; + +export type Stats = Record< + ModuleName, + { + blocked: number; + allowed: number; + withoutContext: number; + total: number; + } +>; + +type Heartbeat = { + type: "heartbeat"; + stats: Stats; + agent: AgentInfo; + time: number; +}; + +export type Event = Started | DetectedAttack | Heartbeat; diff --git a/library/src/agent/api/Token.test.ts b/library/src/agent/api/Token.test.ts new file mode 100644 index 000000000..052f360b9 --- /dev/null +++ b/library/src/agent/api/Token.test.ts @@ -0,0 +1,15 @@ +import * as t from "tap"; +import { Token } from "./Token"; + +t.test("it throws error if token is empty", async (t) => { + t.throws(() => new Token("")); +}); + +t.test("it returns the token as string", async (t) => { + const token = new Token("token"); + t.same(token.asString(), "token"); +}); + +t.test("it throws error if toString() is called", async (t) => { + t.throws(() => `${new Token("token")}`); +}); diff --git a/library/src/agent/api/Token.ts b/library/src/agent/api/Token.ts new file mode 100644 index 000000000..cde7f6740 --- /dev/null +++ b/library/src/agent/api/Token.ts @@ -0,0 +1,15 @@ +export class Token { + constructor(private readonly token: string) { + if (!this.token) { + throw new Error("Token cannot be empty"); + } + } + + toString() { + throw new Error("Please use asString() instead"); + } + + asString() { + return this.token; + } +} diff --git a/library/src/agent/applyHooks.test.ts b/library/src/agent/applyHooks.test.ts index 5db2f14c4..137cc360e 100644 --- a/library/src/agent/applyHooks.test.ts +++ b/library/src/agent/applyHooks.test.ts @@ -1,6 +1,6 @@ import * as t from "tap"; import { applyHooks } from "./applyHooks"; -import { Hooks } from "./Wrapper"; +import { Hooks } from "./hooks/Hooks"; t.test("it ignores if package is not installed", async (t) => { const hooks = new Hooks(); diff --git a/library/src/agent/applyHooks.ts b/library/src/agent/applyHooks.ts index cb13c6659..1f086664d 100644 --- a/library/src/agent/applyHooks.ts +++ b/library/src/agent/applyHooks.ts @@ -2,11 +2,9 @@ import { Hook } from "require-in-the-middle"; import { wrap } from "shimmer"; import { getPackageVersion } from "../helpers/getPackageVersion"; import { satisfiesVersion } from "../helpers/satisfiesVersion"; -import { - ModifyingArgumentsMethodInterceptor, - Hooks, - MethodInterceptor, -} from "./Wrapper"; +import { Hooks } from "./hooks/Hooks"; +import { MethodInterceptor } from "./hooks/MethodInterceptor"; +import { ModifyingArgumentsMethodInterceptor } from "./hooks/ModifyingArgumentsInterceptor"; /** * Hooks allows you to register packages and then wrap specific methods on diff --git a/library/src/agent/Wrapper.test.ts b/library/src/agent/hooks/Hooks.test.ts similarity index 94% rename from library/src/agent/Wrapper.test.ts rename to library/src/agent/hooks/Hooks.test.ts index a5886a425..12b25e4d1 100644 --- a/library/src/agent/Wrapper.test.ts +++ b/library/src/agent/hooks/Hooks.test.ts @@ -1,5 +1,5 @@ import * as t from "tap"; -import { Hooks } from "./Wrapper"; +import { Hooks } from "./Hooks"; t.test("package throws error if name is empty", async (t) => { const hooks = new Hooks(); diff --git a/library/src/agent/hooks/Hooks.ts b/library/src/agent/hooks/Hooks.ts new file mode 100644 index 000000000..ef14398c7 --- /dev/null +++ b/library/src/agent/hooks/Hooks.ts @@ -0,0 +1,20 @@ +import { Package } from "./Package"; + +export class Hooks { + private readonly packages: Package[] = []; + + package(packageName: string): Package { + if (!packageName) { + throw new Error("Package name is required"); + } + + const pkg = new Package(packageName); + this.packages.push(pkg); + + return pkg; + } + + getPackages() { + return this.packages; + } +} diff --git a/library/src/agent/hooks/MethodInterceptor.ts b/library/src/agent/hooks/MethodInterceptor.ts new file mode 100644 index 000000000..7e1fe3670 --- /dev/null +++ b/library/src/agent/hooks/MethodInterceptor.ts @@ -0,0 +1,20 @@ +export type Interceptor = (args: unknown[], subject: unknown) => void; + +export class MethodInterceptor { + constructor( + private readonly name: string, + private readonly interceptor: Interceptor + ) { + if (!this.name) { + throw new Error("Method name is required"); + } + } + + getName() { + return this.name; + } + + getInterceptor() { + return this.interceptor; + } +} diff --git a/library/src/agent/hooks/ModifyingArgumentsInterceptor.ts b/library/src/agent/hooks/ModifyingArgumentsInterceptor.ts new file mode 100644 index 000000000..457cf9211 --- /dev/null +++ b/library/src/agent/hooks/ModifyingArgumentsInterceptor.ts @@ -0,0 +1,23 @@ +export type ModifyingArgumentsInterceptor = ( + args: unknown[], + subject: unknown +) => unknown[]; + +export class ModifyingArgumentsMethodInterceptor { + constructor( + private readonly name: string, + private readonly interceptor: ModifyingArgumentsInterceptor + ) { + if (!this.name) { + throw new Error("Method name is required"); + } + } + + getName() { + return this.name; + } + + getInterceptor() { + return this.interceptor; + } +} diff --git a/library/src/agent/hooks/Package.ts b/library/src/agent/hooks/Package.ts new file mode 100644 index 000000000..da71f9fc1 --- /dev/null +++ b/library/src/agent/hooks/Package.ts @@ -0,0 +1,22 @@ +import { VersionedPackage } from "./VersionedPackage"; + +export class Package { + private versions: VersionedPackage[] = []; + + constructor(private readonly packageName: string) {} + + getName() { + return this.packageName; + } + + withVersion(range: string): VersionedPackage { + const pkg = new VersionedPackage(range); + this.versions.push(pkg); + + return pkg; + } + + getVersions() { + return this.versions; + } +} diff --git a/library/src/agent/hooks/Selector.ts b/library/src/agent/hooks/Selector.ts new file mode 100644 index 000000000..8df00e6b5 --- /dev/null +++ b/library/src/agent/hooks/Selector.ts @@ -0,0 +1,40 @@ +import { Interceptor, MethodInterceptor } from "./MethodInterceptor"; +import { + ModifyingArgumentsInterceptor, + ModifyingArgumentsMethodInterceptor, +} from "./ModifyingArgumentsInterceptor"; + +export class Selector { + private methods: (MethodInterceptor | ModifyingArgumentsMethodInterceptor)[] = + []; + + constructor(private readonly selector: (exports: unknown) => unknown) {} + + inspect(methodName: string, interceptor: Interceptor) { + const method = new MethodInterceptor(methodName, interceptor); + this.methods.push(method); + + return method; + } + + modifyArguments( + methodName: string, + interceptor: ModifyingArgumentsInterceptor + ) { + const method = new ModifyingArgumentsMethodInterceptor( + methodName, + interceptor + ); + this.methods.push(method); + + return method; + } + + getSelector() { + return this.selector; + } + + getMethodInterceptors() { + return this.methods; + } +} diff --git a/library/src/agent/hooks/VersionedPackage.ts b/library/src/agent/hooks/VersionedPackage.ts new file mode 100644 index 000000000..ee83fbb5d --- /dev/null +++ b/library/src/agent/hooks/VersionedPackage.ts @@ -0,0 +1,26 @@ +import { Selector } from "./Selector"; + +export class VersionedPackage { + private selectors: Selector[] = []; + + constructor(private readonly range: string) { + if (!this.range) { + throw new Error("Version range is required"); + } + } + + getRange() { + return this.range; + } + + subject(selector: (exports: any) => unknown): Selector { + const fn = new Selector(selector); + this.selectors.push(fn); + + return fn; + } + + getSelectors() { + return this.selectors; + } +} diff --git a/library/src/agent/logger/Logger.ts b/library/src/agent/logger/Logger.ts new file mode 100644 index 000000000..0e3e4c158 --- /dev/null +++ b/library/src/agent/logger/Logger.ts @@ -0,0 +1,3 @@ +export interface Logger { + log(message: string): void; +} diff --git a/library/src/agent/Logger.ts b/library/src/agent/logger/LoggerConsole.ts similarity index 50% rename from library/src/agent/Logger.ts rename to library/src/agent/logger/LoggerConsole.ts index 69ceb495f..48c9ebd98 100644 --- a/library/src/agent/Logger.ts +++ b/library/src/agent/logger/LoggerConsole.ts @@ -1,11 +1,3 @@ -export interface Logger { - log(message: string): void; -} - -/** - * A console class that has the log function - * @class LoggerConsole - */ export class LoggerConsole { /** * Creates a terminal log with the "AIKIDO: " affix. @@ -15,9 +7,3 @@ export class LoggerConsole { console.log(`AIKIDO: ${message}`); } } - -export class LoggerNoop { - log(message: string) { - // noop - } -} diff --git a/library/src/agent/logger/LoggerNoop.ts b/library/src/agent/logger/LoggerNoop.ts new file mode 100644 index 000000000..db37d5aa4 --- /dev/null +++ b/library/src/agent/logger/LoggerNoop.ts @@ -0,0 +1,5 @@ +export class LoggerNoop { + log(message: string) { + // noop + } +} diff --git a/library/src/agent/protect.ts b/library/src/agent/protect.ts index 416ef9bdb..6efe47307 100644 --- a/library/src/agent/protect.ts +++ b/library/src/agent/protect.ts @@ -1,15 +1,20 @@ import type { APIGatewayProxyHandler } from "aws-lambda"; import { Agent } from "./Agent"; import { getInstance, setInstance } from "./AgentSingleton"; -import { API, APIFetch, APIThrottled, Token } from "./API"; import { Express } from "../sources/Express"; import { createLambdaWrapper } from "../sources/Lambda"; import { MongoDB } from "../sinks/MongoDB"; import { Postgres } from "../sinks/Postgres"; import * as shimmer from "shimmer"; -import { Logger, LoggerConsole, LoggerNoop } from "./Logger"; +import { API } from "./api/API"; +import { APIFetch } from "./api/APIFetch"; +import { APIThrottled } from "./api/APIThrottled"; +import { Token } from "./api/Token"; import { applyHooks } from "./applyHooks"; -import { Hooks } from "./Wrapper"; +import { Hooks } from "./hooks/Hooks"; +import { Logger } from "./logger/Logger"; +import { LoggerConsole } from "./logger/LoggerConsole"; +import { LoggerNoop } from "./logger/LoggerNoop"; import { Options, getOptions } from "../helpers/getOptions"; function wrapInstalledPackages() { diff --git a/library/src/sinks/MongoDB.test.ts b/library/src/sinks/MongoDB.test.ts index dafa18d8d..92ae6f70b 100644 --- a/library/src/sinks/MongoDB.test.ts +++ b/library/src/sinks/MongoDB.test.ts @@ -1,11 +1,12 @@ import * as t from "tap"; import { Agent } from "../agent/Agent"; import { setInstance } from "../agent/AgentSingleton"; -import { APIForTesting, Token } from "../agent/API"; -import { LoggerNoop } from "../agent/Logger"; +import { APIForTesting } from "../agent/api/APIForTesting"; +import { Token } from "../agent/api/Token"; import { Context, runWithContext } from "../agent/Context"; import { applyHooks } from "../agent/applyHooks"; -import { Hooks } from "../agent/Wrapper"; +import { Hooks } from "../agent/hooks/Hooks"; +import { LoggerNoop } from "../agent/logger/LoggerNoop"; import { MongoDB } from "./MongoDB"; const context: Context = { diff --git a/library/src/sinks/MongoDB.ts b/library/src/sinks/MongoDB.ts index 541688fc6..d48e569fa 100644 --- a/library/src/sinks/MongoDB.ts +++ b/library/src/sinks/MongoDB.ts @@ -2,11 +2,12 @@ import { Collection } from "mongodb"; import { Agent } from "../agent/Agent"; import { getInstance } from "../agent/AgentSingleton"; +import { Hooks } from "../agent/hooks/Hooks"; import { detectNoSQLInjection } from "../vulnerabilities/nosql-injection/detectNoSQLInjection"; import { isPlainObject } from "../helpers/isPlainObject"; import { Context, getContext } from "../agent/Context"; import { friendlyName } from "../agent/Source"; -import { Hooks, Wrapper } from "../agent/Wrapper"; +import { Wrapper } from "../agent/Wrapper"; const OPERATIONS_WITH_FILTER = [ "count", diff --git a/library/src/sinks/Postgres.test.ts b/library/src/sinks/Postgres.test.ts index 7e55edacd..61b4fe100 100644 --- a/library/src/sinks/Postgres.test.ts +++ b/library/src/sinks/Postgres.test.ts @@ -1,11 +1,12 @@ import * as t from "tap"; import { Agent } from "../agent/Agent"; import { setInstance } from "../agent/AgentSingleton"; -import { APIForTesting, Token } from "../agent/API"; -import { LoggerNoop } from "../agent/Logger"; +import { APIForTesting } from "../agent/api/APIForTesting"; +import { Token } from "../agent/api/Token"; import { runWithContext, type Context } from "../agent/Context"; import { applyHooks } from "../agent/applyHooks"; -import { Hooks } from "../agent/Wrapper"; +import { Hooks } from "../agent/hooks/Hooks"; +import { LoggerNoop } from "../agent/logger/LoggerNoop"; import { Postgres } from "./Postgres"; import type { Client } from "pg"; diff --git a/library/src/sinks/Postgres.ts b/library/src/sinks/Postgres.ts index 7aea3cb4d..20b54d337 100644 --- a/library/src/sinks/Postgres.ts +++ b/library/src/sinks/Postgres.ts @@ -1,4 +1,5 @@ -import { Hooks, Wrapper } from "../agent/Wrapper"; +import { Hooks } from "../agent/hooks/Hooks"; +import { Wrapper } from "../agent/Wrapper"; import { getInstance } from "../agent/AgentSingleton"; import { getContext } from "../agent/Context"; import { checkContextForSqlInjection } from "../vulnerabilities/sql-injection/detectSQLInjection"; diff --git a/library/src/sources/Express.test.ts b/library/src/sources/Express.test.ts index 9ec01ff69..0e1b8cbbc 100644 --- a/library/src/sources/Express.test.ts +++ b/library/src/sources/Express.test.ts @@ -1,6 +1,6 @@ import * as t from "tap"; import { applyHooks } from "../agent/applyHooks"; -import { Hooks } from "../agent/Wrapper"; +import { Hooks } from "../agent/hooks/Hooks"; import { Express } from "./Express"; // Before express is required! diff --git a/library/src/sources/Express.ts b/library/src/sources/Express.ts index 45af76450..85b01b40d 100644 --- a/library/src/sources/Express.ts +++ b/library/src/sources/Express.ts @@ -1,7 +1,8 @@ /* eslint-disable prefer-rest-params */ import type { NextFunction, Request, Response } from "express"; import { runWithContext } from "../agent/Context"; -import { Hooks, Wrapper } from "../agent/Wrapper"; +import { Hooks } from "../agent/hooks/Hooks"; +import { Wrapper } from "../agent/Wrapper"; import { METHODS } from "node:http"; type Middleware = (req: Request, resp: Response, next: NextFunction) => void;