Skip to content

Commit 62d750c

Browse files
authored
Merge pull request #454 from AikidoSec/poc-required-pkg
Report packages used at runtime
2 parents 2ce642a + 8560275 commit 62d750c

File tree

7 files changed

+157
-18
lines changed

7 files changed

+157
-18
lines changed

library/agent/Agent.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { wrapInstalledPackages } from "./wrapInstalledPackages";
2525
import { Wrapper } from "./Wrapper";
2626
import { isAikidoCI } from "../helpers/isAikidoCI";
2727
import { AttackLogger } from "./AttackLogger";
28+
import { Packages } from "./Packages";
2829

2930
type WrappedPackage = { version: string | null; supported: boolean };
3031

@@ -38,6 +39,7 @@ export class Agent {
3839
private preventedPrototypePollution = false;
3940
private incompatiblePackages: Record<string, string> = {};
4041
private wrappedPackages: Record<string, WrappedPackage> = {};
42+
private packages = new Packages();
4143
private timeoutInMS = 30 * 1000;
4244
private hostnames = new Hostnames(200);
4345
private users = new Users(1000);
@@ -296,11 +298,13 @@ export class Agent {
296298
const routes = this.routes.asArray();
297299
const outgoingDomains = this.hostnames.asArray();
298300
const users = this.users.asArray();
301+
const packages = this.packages.asArray();
299302
const endedAt = Date.now();
300303
this.statistics.reset();
301304
this.routes.clear();
302305
this.hostnames.clear();
303306
this.users.clear();
307+
this.packages.clear();
304308
const response = await this.api.report(
305309
this.token,
306310
{
@@ -315,6 +319,7 @@ export class Agent {
315319
userAgents: stats.userAgents,
316320
ipAddresses: stats.ipAddresses,
317321
},
322+
packages,
318323
hostnames: outgoingDomains,
319324
routes: routes,
320325
users: users,
@@ -479,6 +484,10 @@ export class Agent {
479484
}
480485
}
481486

487+
// When our library is required, we are not intercepting `require` calls yet
488+
// We need to add our library to the list of packages manually
489+
this.onPackageRequired("@aikido/firewall", getAgentVersion());
490+
482491
wrapInstalledPackages(wrappers, this.serverless);
483492

484493
// Send startup event and wait for config
@@ -503,11 +512,19 @@ export class Agent {
503512
this.logger.log(`Failed to wrap module ${module}: ${error.message}`);
504513
}
505514

515+
onPackageRequired(name: string, version: string) {
516+
this.packages.addPackage({
517+
name,
518+
version,
519+
});
520+
}
521+
506522
onPackageWrapped(name: string, details: WrappedPackage) {
507523
if (this.wrappedPackages[name]) {
508524
// Already reported as wrapped
509525
return;
510526
}
527+
511528
this.wrappedPackages[name] = details;
512529

513530
if (details.version) {

library/agent/Packages.test.ts

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import * as t from "tap";
2+
import * as FakeTimers from "@sinonjs/fake-timers";
3+
import { Packages } from "./Packages";
4+
5+
t.test("addPackage should add a new package", async (t) => {
6+
const clock = FakeTimers.install();
7+
const packages = new Packages();
8+
packages.addPackage({ name: "express", version: "4.17.1" });
9+
const arr = packages.asArray();
10+
t.same(arr, [{ name: "express", version: "4.17.1", requiredAt: 0 }]);
11+
clock.uninstall();
12+
});
13+
14+
t.test(
15+
"addPackage should add a new version for an existing package",
16+
async (t) => {
17+
const clock = FakeTimers.install();
18+
const packages = new Packages();
19+
packages.addPackage({ name: "lodash", version: "4.17.20" });
20+
clock.tick(10);
21+
packages.addPackage({ name: "lodash", version: "4.17.21" });
22+
const arr = packages.asArray();
23+
t.same(arr, [
24+
{ name: "lodash", version: "4.17.20", requiredAt: 0 },
25+
{ name: "lodash", version: "4.17.21", requiredAt: 10 },
26+
]);
27+
clock.uninstall();
28+
}
29+
);
30+
31+
t.test("addPackage should not add a duplicate package version", async (t) => {
32+
const clock = FakeTimers.install();
33+
const packages = new Packages();
34+
packages.addPackage({ name: "moment", version: "2.29.1" });
35+
packages.addPackage({ name: "moment", version: "2.29.1" });
36+
const arr = packages.asArray();
37+
t.same(arr, [{ name: "moment", version: "2.29.1", requiredAt: 0 }]);
38+
clock.uninstall();
39+
});
40+
41+
t.test(
42+
"asArray should return an empty array when no packages are added",
43+
async (t) => {
44+
const packages = new Packages();
45+
const arr = packages.asArray();
46+
t.same(arr, [], "should return an empty array");
47+
}
48+
);
49+
50+
t.test("asArray should return all packages and versions", async (t) => {
51+
const clock = FakeTimers.install();
52+
const packages = new Packages();
53+
packages.addPackage({ name: "express", version: "4.17.1" });
54+
clock.tick(5);
55+
packages.addPackage({ name: "lodash", version: "4.17.20" });
56+
clock.tick(15);
57+
packages.addPackage({ name: "lodash", version: "4.17.21" });
58+
const arr = packages.asArray();
59+
t.same(arr, [
60+
{ name: "express", version: "4.17.1", requiredAt: 0 },
61+
{ name: "lodash", version: "4.17.20", requiredAt: 5 },
62+
{ name: "lodash", version: "4.17.21", requiredAt: 20 },
63+
]);
64+
clock.uninstall();
65+
});
66+
67+
t.test("clear should remove all packages", async (t) => {
68+
const clock = FakeTimers.install();
69+
const packages = new Packages();
70+
packages.addPackage({ name: "express", version: "4.17.1" });
71+
packages.addPackage({ name: "lodash", version: "4.17.20" });
72+
packages.clear();
73+
const arr = packages.asArray();
74+
t.same(arr, [], "should return an empty array after clear");
75+
clock.uninstall();
76+
});

library/agent/Packages.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
type PackageInfo = {
2+
name: string;
3+
version: string;
4+
requiredAt: number;
5+
};
6+
7+
export class Packages {
8+
private packages: Map<string, PackageInfo[]> = new Map();
9+
10+
addPackage(pkg: { name: string; version: string }) {
11+
const versions = this.packages.get(pkg.name) || [];
12+
const existingVersion = versions.find((v) => v.version === pkg.version);
13+
14+
if (existingVersion) {
15+
return;
16+
}
17+
18+
versions.push({
19+
name: pkg.name,
20+
version: pkg.version,
21+
requiredAt: Date.now(),
22+
});
23+
24+
this.packages.set(pkg.name, versions);
25+
}
26+
27+
asArray() {
28+
return Array.from(this.packages.values()).flat();
29+
}
30+
31+
clear() {
32+
this.packages.clear();
33+
}
34+
}

library/agent/api/Event.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,11 @@ type Heartbeat = {
112112
breakdown: Record<string, number>;
113113
};
114114
};
115+
packages: {
116+
name: string;
117+
version: string;
118+
requiredAt: number;
119+
}[];
115120
hostnames: { hostname: string; port: number | undefined; hits: number }[];
116121
routes: {
117122
path: string;

library/agent/api/ReportingAPIRateLimitedClientSide.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,7 @@ function generateHeartbeatEvent(): Event {
188188
hostnames: [],
189189
routes: [],
190190
users: [],
191+
packages: [],
191192
};
192193
}
193194

library/agent/hooks/wrapRequire.ts

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -183,21 +183,21 @@ function patchPackage(this: mod, id: string, originalExports: unknown) {
183183
const moduleName = pathInfo.name;
184184

185185
// Get all versioned packages for the module name
186-
const versionedPackages = packages
186+
const versionedPackagesToInstrument = packages
187187
.filter((pkg) => pkg.getName() === moduleName)
188188
.map((pkg) => pkg.getVersions())
189189
.flat();
190190

191-
// We don't want to patch this package because we do not have any hooks for it
192-
if (!versionedPackages.length) {
191+
// Read the package.json of the required package
192+
let packageJson: PackageJson | undefined;
193+
try {
194+
packageJson = originalRequire(
195+
`${pathInfo.base}/package.json`
196+
) as PackageJson;
197+
} catch {
193198
return originalExports;
194199
}
195200

196-
// Read the package.json of the required package
197-
const packageJson = originalRequire(
198-
`${pathInfo.base}/package.json`
199-
) as PackageJson;
200-
201201
// Get the version of the installed package
202202
const installedPkgVersion = packageJson.version;
203203
if (!installedPkgVersion) {
@@ -206,19 +206,24 @@ function patchPackage(this: mod, id: string, originalExports: unknown) {
206206
);
207207
}
208208

209+
const agent = getInstance();
210+
agent?.onPackageRequired(moduleName, installedPkgVersion);
211+
212+
// We don't want to patch this package because we do not have any hooks for it
213+
if (!versionedPackagesToInstrument.length) {
214+
return originalExports;
215+
}
216+
209217
// Check if the installed package version is supported (get all matching versioned packages)
210-
const matchingVersionedPackages = versionedPackages.filter((pkg) =>
211-
satisfiesVersion(pkg.getRange(), installedPkgVersion)
218+
const matchingVersionedPackages = versionedPackagesToInstrument.filter(
219+
(pkg) => satisfiesVersion(pkg.getRange(), installedPkgVersion)
212220
);
213221

214-
const agent = getInstance();
215-
if (agent) {
216-
// Report to the agent that the package was wrapped or not if it's version is not supported
217-
agent.onPackageWrapped(moduleName, {
218-
version: installedPkgVersion,
219-
supported: !!matchingVersionedPackages.length,
220-
});
221-
}
222+
// Report to the agent that the package was wrapped or not if it's version is not supported
223+
agent?.onPackageWrapped(moduleName, {
224+
version: installedPkgVersion,
225+
supported: !!matchingVersionedPackages.length,
226+
});
222227

223228
if (!matchingVersionedPackages.length) {
224229
// We don't want to patch this package version

library/sources/Lambda.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ t.test("it sends heartbeat after first and every 10 minutes", async () => {
272272
hostnames: [],
273273
routes: [],
274274
users: [],
275+
packages: [],
275276
stats: {
276277
operations: {
277278
"mongodb.query": {

0 commit comments

Comments
 (0)