diff --git a/.changeset/seven-feet-lick.md b/.changeset/seven-feet-lick.md new file mode 100644 index 00000000..e45e5387 --- /dev/null +++ b/.changeset/seven-feet-lick.md @@ -0,0 +1,5 @@ +--- +"@nodesecure/scanner": minor +--- + +Implement EventEmitter with two events on Payload class (Extractors) diff --git a/workspaces/scanner/docs/extractors.md b/workspaces/scanner/docs/extractors.md index c257e67f..9ef2475e 100644 --- a/workspaces/scanner/docs/extractors.md +++ b/workspaces/scanner/docs/extractors.md @@ -75,6 +75,7 @@ export interface ManifestProbeExtractor extends ProbeExtractor { > [!NOTE] > generic `T` is defined as extending from `ProbeExtractor[]` +The `Payload` class extends Node.js `EventEmitter`, allowing you to listen for extraction events and implement custom behavior during the extraction process. ### constructor(data: Scanner.Payload | Scanner.Payload[ "dependencies" ], probes: [ ...T ]) @@ -104,3 +105,32 @@ Runs the probes and deeply merges their results into a single record, for exampl "probe2": "xxx" } ``` + +### Events + +Since `Payload` extends `EventEmitter`, you can listen for events that are emitted during the extraction process, the parameters signature of each listener is the same as the `next()` method of its corresponding probe. + + +### packument + +Emitted for each dependency when processing at the packument level, the event signature is: + +```ts +extractor.on('packument', (name: string, dependency: Scanner.Dependency) => { + // Handle packument-level processing +}); +``` + +#### manifest + +Emitted for each dependency version when processing at the manifest level, the event signature is: + +```ts +extractor.on('manifest', ( + spec: string, + depVersion: Scanner.DependencyVersion, + parent: { name: string, dependency: Scanner.Dependency } +) => { + // Handle manifest-level processing +}); +``` \ No newline at end of file diff --git a/workspaces/scanner/src/extractors/payload.ts b/workspaces/scanner/src/extractors/payload.ts index b00b8e44..d7e8330f 100644 --- a/workspaces/scanner/src/extractors/payload.ts +++ b/workspaces/scanner/src/extractors/payload.ts @@ -1,3 +1,6 @@ +// Import Node.js Dependencies +import { EventEmitter } from "node:events"; + // Import Third-party Dependencies import type { Simplify } from "type-fest"; // @ts-ignore @@ -48,7 +51,7 @@ export interface ManifestProbeExtractor extends ProbeExtractor { ): void; } -export class Payload[]> { +export class Payload[]> extends EventEmitter { private dependencies: Scanner.Payload["dependencies"]; private probes: Record; private cachedResult: ExtractProbeResult; @@ -57,6 +60,7 @@ export class Payload[]> { data: Scanner.Payload | Scanner.Payload["dependencies"], probes: [...T] ) { + super(); this.dependencies = isNodesecurePayload(data) ? data.dependencies : data; @@ -75,9 +79,11 @@ export class Payload[]> { for (const [name, dependency] of Object.entries(this.dependencies)) { this.probes.packument.forEach((probe) => probe.next(name, dependency)); + this.emit("packument", name, dependency); if (this.probes.manifest.length > 0) { for (const [spec, depVersion] of Object.entries(dependency.versions)) { this.probes.manifest.forEach((probe) => probe.next(spec, depVersion, { name, dependency })); + this.emit("manifest", spec, depVersion, { name, dependency }); } } } diff --git a/workspaces/scanner/test/extractors/payload.spec.ts b/workspaces/scanner/test/extractors/payload.spec.ts index d9481612..73a733b0 100644 --- a/workspaces/scanner/test/extractors/payload.spec.ts +++ b/workspaces/scanner/test/extractors/payload.spec.ts @@ -254,3 +254,45 @@ describe("Extractors.Probes", () => { ); }); }); + +describe("Events", () => { + it("should emits packument and manifest events", () => { + const vulnerabilitiesExtractor = new Extractors.Probes.VulnerabilitiesExtractor(); + const licensesExtractor = new Extractors.Probes.LicensesExtractor(); + type ManifestEvent = Parameters; + type PackumentEvent = Parameters; + + const extractor = new Extractors.Payload( + expressNodesecurePayload, + [ + licensesExtractor, + vulnerabilitiesExtractor + ] + ); + + const manifestEvents: ManifestEvent[] = []; + const packumentEvents: PackumentEvent[] = []; + + extractor.on("manifest", (...event: ManifestEvent) => { + manifestEvents.push(event); + }); + + extractor.on("packument", (...event: PackumentEvent) => { + packumentEvents.push(event); + }); + + const dependencies = Object.entries(expressNodesecurePayload.dependencies); + + const expectedPackumentEvents = dependencies; + const expectedManifestEvents = dependencies.flatMap(([name, dependency]) => Object + .entries(dependency.versions) + .map( + ([spec, depVersion]) => [spec, depVersion, { name, dependency }] + ) + ); + + extractor.extract(); + assert.deepEqual(packumentEvents, expectedPackumentEvents); + assert.deepEqual(manifestEvents, expectedManifestEvents); + }); +});