Skip to content

Commit 7330ef9

Browse files
clemgbldfraxken
andauthored
feat(scanner): integrate buit-in stats in the response of depWalker (#612)
* feat(scanner): integrate built-in stats in the response of depWalker * Update workspaces/scanner/src/class/DateProvider.class.ts Co-authored-by: Thomas.G <gentilhomme.thomas@gmail.com> * Update workspaces/scanner/src/class/StatsCollector.class.ts Co-authored-by: Thomas.G <gentilhomme.thomas@gmail.com> * Update workspaces/scanner/src/class/StatsCollector.class.ts Co-authored-by: Thomas.G <gentilhomme.thomas@gmail.com> --------- Co-authored-by: Thomas.G <gentilhomme.thomas@gmail.com>
1 parent 9b0ba2e commit 7330ef9

9 files changed

Lines changed: 251 additions & 39 deletions

File tree

.changeset/wet-bears-notice.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@nodesecure/scanner": minor
3+
---
4+
5+
feat(scanner): integrate built-in stats in the response of depWalker
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
export interface DateProvider {
2+
now(): number;
3+
oneYearAgo(): Date;
4+
}
5+
6+
export class SystemDateProvider implements DateProvider {
7+
now(): number {
8+
return Date.now();
9+
}
10+
11+
oneYearAgo(): Date {
12+
const date = new Date();
13+
date.setFullYear(date.getFullYear() - 1);
14+
15+
return date;
16+
}
17+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Import Internal Dependencies
2+
import { SystemDateProvider, type DateProvider } from "./DateProvider.class.ts";
3+
import type { ApiStats, Stats } from "../types.ts";
4+
5+
export class StatsCollector {
6+
#apiCalls: ApiStats[] = [];
7+
#dateProvider: DateProvider;
8+
#startedAt: number;
9+
10+
constructor(dateProvider: DateProvider = new SystemDateProvider()) {
11+
this.#dateProvider = dateProvider;
12+
this.#startedAt = this.#dateProvider.now();
13+
}
14+
15+
track<T extends () => any>(name: string, fn: T): ReturnType<T> {
16+
const startedAt = this.#dateProvider.now();
17+
try {
18+
const result = fn();
19+
if (result instanceof Promise) {
20+
return result.finally(() => this.#addApiStat(name, startedAt)
21+
) as ReturnType<T>;
22+
}
23+
24+
this.#addApiStat(name, startedAt);
25+
26+
return result;
27+
}
28+
catch (err) {
29+
this.#addApiStat(name, startedAt);
30+
throw err;
31+
}
32+
}
33+
34+
#addApiStat(name: string, startedAt: number) {
35+
this.#apiCalls.push({
36+
name,
37+
startedAt,
38+
executionTime: this.#dateProvider.now() - startedAt
39+
});
40+
}
41+
42+
getStats(): Stats {
43+
return {
44+
startedAt: this.#startedAt,
45+
executionTime: this.#dateProvider.now() - this.#startedAt,
46+
apiCalls: this.#apiCalls,
47+
apiCallsCount: this.#apiCalls.length
48+
};
49+
}
50+
}

workspaces/scanner/src/depWalker.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import path from "node:path";
33
import { readFileSync } from "node:fs";
44

55
// Import Third-party Dependencies
6+
import pacote from "pacote";
7+
import * as npmRegistrySDK from "@nodesecure/npm-registry-sdk";
68
import { Mutex, MutexRelease } from "@openally/mutex";
79
import {
810
extractAndResolve,
@@ -26,7 +28,8 @@ import {
2628
getManifestLinks,
2729
NPM_TOKEN
2830
} from "./utils/index.ts";
29-
import { NpmRegistryProvider } from "./registry/NpmRegistryProvider.ts";
31+
import { NpmRegistryProvider, type NpmApiClient } from "./registry/NpmRegistryProvider.ts";
32+
import { StatsCollector } from "./class/StatsCollector.class.ts";
3033
import { RegistryTokenStore } from "./registry/RegistryTokenStore.ts";
3134
import { TempDirectory } from "./class/TempDirectory.class.ts";
3235
import { Logger, ScannerLoggerEvents } from "./class/logger.class.ts";
@@ -94,7 +97,6 @@ type InitialPayload =
9497
Partial<Payload> &
9598
{
9699
rootDependency: Payload["rootDependency"];
97-
metadata: Payload["metadata"];
98100
};
99101

100102
export async function depWalker(
@@ -113,7 +115,7 @@ export async function depWalker(
113115
npmRcConfig
114116
} = options;
115117

116-
const startedAt = Date.now();
118+
const statsCollector = new StatsCollector();
117119
const isRemoteScanning = typeof location === "undefined";
118120
const tokenStore = new RegistryTokenStore(npmRcConfig, NPM_TOKEN.token);
119121

@@ -130,18 +132,36 @@ export async function depWalker(
130132
},
131133
scannerVersion: packageVersion,
132134
vulnerabilityStrategy,
133-
warnings: [],
134-
metadata: {
135-
startedAt,
136-
executionTime: 0
137-
}
135+
warnings: []
138136
};
139137

140138
const dependencies: Map<string, Dependency> = new Map();
141139
const highlightedPackages: Set<string> = new Set();
142140
const npmTreeWalker = new npm.TreeWalker({
143-
registry
141+
registry,
142+
providers: {
143+
pacote: {
144+
manifest: (spec, opts) => statsCollector.track(`pacote.manifest ${spec}`, () => pacote.manifest(spec, opts)),
145+
packument: (spec, opts) => statsCollector.track(`pacote.packument ${spec}`, () => pacote.packument(spec, opts))
146+
}
147+
}
144148
});
149+
const npmApiClient: NpmApiClient = {
150+
packument: (name, opts) => statsCollector.track(
151+
`npmRegistrySDK.packument ${name}`,
152+
() => npmRegistrySDK.packument(name, opts)
153+
),
154+
155+
packumentVersion: (name, version, opts) => statsCollector.track(
156+
`npmRegistrySDK.packumentVersion ${name}@${version}`,
157+
() => npmRegistrySDK.packumentVersion(name, version, opts)
158+
),
159+
160+
org: (namespace) => statsCollector.track(
161+
`npmRegistrySDK.org ${namespace}`,
162+
() => npmRegistrySDK.org(namespace)
163+
)
164+
};
145165
{
146166
logger
147167
.start(ScannerLoggerEvents.analysis.tree)
@@ -181,7 +201,8 @@ export async function depWalker(
181201
operationsQueue.push(
182202
new NpmRegistryProvider(name, version, {
183203
registry,
184-
tokenStore
204+
tokenStore,
205+
npmApiClient
185206
}).enrichDependencyVersion(dep, dependencyConfusionWarnings, org)
186207
);
187208

@@ -350,7 +371,7 @@ export async function depWalker(
350371
packages: [...highlightedPackages]
351372
};
352373
payload.dependencies = Object.fromEntries(dependencies);
353-
payload.metadata.executionTime = Date.now() - startedAt;
374+
payload.metadata = statsCollector.getStats();
354375

355376
return payload as Payload;
356377
}

workspaces/scanner/src/registry/NpmRegistryProvider.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@ import * as i18n from "@nodesecure/i18n";
1111
import { isHTTPError } from "@openally/httpie";
1212

1313
// Import Internal Dependencies
14-
import { PackumentExtractor, type DateProvider } from "./PackumentExtractor.ts";
14+
import { PackumentExtractor } from "./PackumentExtractor.ts";
15+
import type { DateProvider } from "../class/DateProvider.class.ts";
1516
import { fetchNpmAvatars } from "./fetchNpmAvatars.ts";
1617
import type {
1718
Dependency,

workspaces/scanner/src/registry/PackumentExtractor.ts

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,7 @@ import { packageJSONIntegrityHash } from "@nodesecure/mama";
66
import type {
77
Dependency
88
} from "../types.ts";
9-
10-
export interface DateProvider {
11-
oneYearAgo(): Date;
12-
}
9+
import { SystemDateProvider, type DateProvider } from "../class/DateProvider.class.ts";
1310

1411
export interface PackumentExtractorOptions {
1512
dateProvider?: DateProvider;
@@ -103,12 +100,3 @@ export class PackumentExtractor {
103100
return result;
104101
}
105102
}
106-
107-
class SystemDateProvider implements DateProvider {
108-
oneYearAgo(): Date {
109-
const date = new Date();
110-
date.setFullYear(date.getFullYear() - 1);
111-
112-
return date;
113-
}
114-
}

workspaces/scanner/src/types.ts

Lines changed: 35 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,40 @@ export type GlobalWarning = { message: string; } & (
184184
|
185185
DependencyConfusionWarning);
186186

187+
export type ApiStats = {
188+
/**
189+
* UNIX Timestamp just before the api call start
190+
*/
191+
startedAt: number;
192+
/**
193+
* Execution time in milliseconds
194+
*/
195+
executionTime: number;
196+
197+
/**
198+
* Name of the api call
199+
*/
200+
name: string;
201+
};
202+
203+
export type Stats = {
204+
/**
205+
* UNIX Timestamp when the scan started
206+
*/
207+
startedAt: number;
208+
/**
209+
* Execution time in milliseconds
210+
*/
211+
executionTime: number;
212+
213+
/**
214+
* Number of external API calls
215+
*/
216+
apiCallsCount: number;
217+
218+
apiCalls: ApiStats[];
219+
};
220+
187221
export interface Payload {
188222
/** Payload unique id */
189223
id: string;
@@ -207,16 +241,7 @@ export interface Payload {
207241
/** Vulnerability strategy name (npm, snyk, node) */
208242
vulnerabilityStrategy: Vulnera.Kind;
209243

210-
metadata: {
211-
/**
212-
* UNIX Timestamp when the scan started
213-
*/
214-
startedAt: number;
215-
/**
216-
* Execution time in milliseconds
217-
*/
218-
executionTime: number;
219-
};
244+
metadata: Stats;
220245
}
221246

222247
export type SemverRange = string | "*";
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
// Import Node.js Dependencies
2+
import { describe, it } from "node:test";
3+
import assert from "node:assert";
4+
5+
// Import Internal Dependencies
6+
import type { DateProvider } from "../src/class/DateProvider.class.ts";
7+
import { StatsCollector } from "../src/class/StatsCollector.class.ts";
8+
9+
describe("StatsCollectors", () => {
10+
it("should get the expected global start and execution time", () => {
11+
const dateProvider = new FakeDateProvider();
12+
dateProvider.setNow(1658512000000);
13+
const statsCollector = new StatsCollector(dateProvider);
14+
dateProvider.setNow(1658512001000);
15+
const { startedAt, executionTime } = statsCollector.getStats();
16+
assert.strictEqual(startedAt, 1658512000000);
17+
assert.strictEqual(executionTime, 1000);
18+
});
19+
20+
it("should still record the exexution time if the function being tracked throws", () => {
21+
const dateProvider = new FakeDateProvider();
22+
dateProvider.setNow(1658512000000);
23+
const statsCollector = new StatsCollector(dateProvider);
24+
assert.throws(() => {
25+
statsCollector.track("api/test/1", () => {
26+
dateProvider.setNow(1658512001000);
27+
throw new Error("oh no!");
28+
});
29+
});
30+
31+
const { apiCalls, apiCallsCount } = statsCollector.getStats();
32+
assert.strictEqual(apiCallsCount, 1);
33+
assert.deepEqual(apiCalls, [
34+
{
35+
name: "api/test/1",
36+
startedAt: 1658512000000,
37+
executionTime: 1000
38+
}
39+
40+
]);
41+
});
42+
43+
it("should be able to track the start and execution time of external api call", async() => {
44+
let hasFnOneBeenCalled = false;
45+
let hasFnTwoBeenCalled = false;
46+
const dateProvider = new FakeDateProvider();
47+
dateProvider.setNow(1658512000000);
48+
const statsCollector = new StatsCollector(dateProvider);
49+
dateProvider.setNow(1658512001001);
50+
const promise = statsCollector.track("api/test/1", () => {
51+
hasFnOneBeenCalled = true;
52+
53+
return Promise.resolve(1);
54+
});
55+
56+
dateProvider.setNow(1658512002000);
57+
const promiseResult = await promise;
58+
59+
dateProvider.setNow(1658512003000);
60+
const fnResult = statsCollector.track("api/test/2", () => {
61+
hasFnTwoBeenCalled = true;
62+
dateProvider.setNow(1658512004000);
63+
64+
return null;
65+
});
66+
dateProvider.setNow(1658512005000);
67+
const { apiCalls, apiCallsCount } = statsCollector.getStats();
68+
assert.strictEqual(promiseResult, 1);
69+
assert.strictEqual(fnResult, null);
70+
assert.strictEqual(hasFnOneBeenCalled, true);
71+
assert.strictEqual(hasFnTwoBeenCalled, true);
72+
assert.strictEqual(apiCallsCount, 2);
73+
assert.deepEqual(apiCalls, [
74+
{
75+
name: "api/test/1",
76+
startedAt: 1658512001001,
77+
executionTime: 999
78+
},
79+
{
80+
name: "api/test/2",
81+
startedAt: 1658512003000,
82+
executionTime: 1000
83+
}
84+
]);
85+
});
86+
});
87+
88+
class FakeDateProvider implements DateProvider {
89+
#now: number;
90+
now(): number {
91+
return this.#now;
92+
}
93+
oneYearAgo(): Date {
94+
return new Date(Date.now() - (365 * 24 * 60 * 60 * 1000));
95+
}
96+
97+
setNow(now: number) {
98+
this.#now = now;
99+
}
100+
}

0 commit comments

Comments
 (0)