From a075e1377aeb7541a7383e38343c555aa06bb6dc Mon Sep 17 00:00:00 2001 From: fraxken Date: Mon, 2 Jun 2025 17:13:17 +0200 Subject: [PATCH] refactor(scanner/extractors): simply APIs & add fast probes with callbacks --- .changeset/afraid-cows-eat.md | 5 + workspaces/scanner/docs/extractors.md | 16 +- workspaces/scanner/src/extractors/index.ts | 10 +- workspaces/scanner/src/extractors/payload.ts | 39 ++++- .../probes/ContactExtractor.class.ts | 6 +- .../extractors/probes/FlagsExtractor.class.ts | 4 +- .../probes/LicensesExtractor.class.ts | 6 +- .../extractors/probes/SizeExtractor.class.ts | 8 +- .../probes/VulnerabilitiesExtractor.class.ts | 4 +- .../probes/WarningsExtractor.class.ts | 8 +- .../scanner/test/extractors/payload.spec.ts | 143 +++++++++++++++--- 11 files changed, 192 insertions(+), 57 deletions(-) create mode 100644 .changeset/afraid-cows-eat.md diff --git a/.changeset/afraid-cows-eat.md b/.changeset/afraid-cows-eat.md new file mode 100644 index 00000000..88556956 --- /dev/null +++ b/.changeset/afraid-cows-eat.md @@ -0,0 +1,5 @@ +--- +"@nodesecure/scanner": minor +--- + +Simplify extractors name & add way to inject fast probes with callbacks" diff --git a/workspaces/scanner/docs/extractors.md b/workspaces/scanner/docs/extractors.md index 9ef2475e..692c8967 100644 --- a/workspaces/scanner/docs/extractors.md +++ b/workspaces/scanner/docs/extractors.md @@ -14,7 +14,7 @@ const payload = await from("fastify"); const extractor = new Extractors.Payload( payload, [ - new Extractors.Probes.ContactExtractor() + new Extractors.Probes.Contacts() ] ); @@ -31,12 +31,12 @@ Available probes include: | name | level | | --- | --- | -| ContactExtractor | manifest | -| LicensesExtractor | manifest | -| SizeExtractor | manifest | -| FlagsExtractor | manifest | -| VulnerabilitiesExtractor | packument | -| WarningsExtractor | manifest | +| Contacts | manifest | +| Licenses | manifest | +| Size | manifest | +| Flags | manifest | +| Vulnerabilities | packument | +| Warnings | manifest | All probes follow the same `ProbeExtractor` interface, which acts as an iterator-like contract: @@ -133,4 +133,4 @@ extractor.on('manifest', ( ) => { // Handle manifest-level processing }); -``` \ No newline at end of file +``` diff --git a/workspaces/scanner/src/extractors/index.ts b/workspaces/scanner/src/extractors/index.ts index 6d26135e..0d5d6fb0 100644 --- a/workspaces/scanner/src/extractors/index.ts +++ b/workspaces/scanner/src/extractors/index.ts @@ -1,20 +1,26 @@ // Import Internal Dependencies import { Payload, + Callbacks, type ProbeExtractor, type PackumentProbeExtractor, - type ManifestProbeExtractor + type ManifestProbeExtractor, + type PackumentProbeNextCallback, + type ManifestProbeNextCallback } from "./payload.js"; import * as Probes from "./probes/index.js"; export const Extractors = { Payload, + Callbacks, Probes } as const; export type { ProbeExtractor, PackumentProbeExtractor, - ManifestProbeExtractor + ManifestProbeExtractor, + PackumentProbeNextCallback, + ManifestProbeNextCallback }; diff --git a/workspaces/scanner/src/extractors/payload.ts b/workspaces/scanner/src/extractors/payload.ts index d7e8330f..b425d335 100644 --- a/workspaces/scanner/src/extractors/payload.ts +++ b/workspaces/scanner/src/extractors/payload.ts @@ -31,6 +31,12 @@ export type ProbeExtractorManifestParent = { dependency: Scanner.Dependency; }; +export type PackumentProbeNextCallback = (name: string, dependency: Scanner.Dependency) => void; +export type ManifestProbeNextCallback = ( + spec: string, + dependencyVersion: Scanner.DependencyVersion, + parent: ProbeExtractorManifestParent) => void; + export interface ProbeExtractor { level: ProbeExtractorLevel; next(...args: any[]): void; @@ -39,16 +45,12 @@ export interface ProbeExtractor { export interface PackumentProbeExtractor extends ProbeExtractor { level: "packument"; - next(name: string, dependency: Scanner.Dependency): void; + next: PackumentProbeNextCallback; } export interface ManifestProbeExtractor extends ProbeExtractor { level: "manifest"; - next( - spec: string, - dependencyVersion: Scanner.DependencyVersion, - parent: ProbeExtractorManifestParent - ): void; + next: ManifestProbeNextCallback; } export class Payload[]> extends EventEmitter { @@ -102,3 +104,28 @@ export class Payload[]> extends EventEmitter { ) as unknown as MergedExtractProbeResult; } } + +export const Callbacks = { + packument( + callback: PackumentProbeNextCallback + ): PackumentProbeExtractor { + return { + level: "packument" as const, + next: callback, + done: noop + }; + }, + manifest( + callback: ManifestProbeNextCallback + ): ManifestProbeExtractor { + return { + level: "manifest" as const, + next: callback, + done: noop + }; + } +} as const; + +function noop() { + return void 0; +} diff --git a/workspaces/scanner/src/extractors/probes/ContactExtractor.class.ts b/workspaces/scanner/src/extractors/probes/ContactExtractor.class.ts index 34c32192..1f4a0450 100644 --- a/workspaces/scanner/src/extractors/probes/ContactExtractor.class.ts +++ b/workspaces/scanner/src/extractors/probes/ContactExtractor.class.ts @@ -8,14 +8,14 @@ import type { } from "../payload.js"; import type { DependencyVersion } from "../../types.js"; -export type ContactExtractorResult = { +export type ContactsResult = { contacts: Record; }; -export class ContactExtractor implements ManifestProbeExtractor { +export class Contacts implements ManifestProbeExtractor { level = "manifest" as const; - #contacts: ContactExtractorResult["contacts"] = Object.create(null); + #contacts: ContactsResult["contacts"] = Object.create(null); #packages: Set = new Set(); #addContact( diff --git a/workspaces/scanner/src/extractors/probes/FlagsExtractor.class.ts b/workspaces/scanner/src/extractors/probes/FlagsExtractor.class.ts index bc32a2dc..58b7f69d 100644 --- a/workspaces/scanner/src/extractors/probes/FlagsExtractor.class.ts +++ b/workspaces/scanner/src/extractors/probes/FlagsExtractor.class.ts @@ -7,11 +7,11 @@ import type { } from "../payload.js"; import type { DependencyVersion } from "../../types.js"; -export type FlagsExtractorResult = { +export type FlagsResult = { flags: Record; }; -export class FlagsExtractor implements ManifestProbeExtractor { +export class Flags implements ManifestProbeExtractor { level = "manifest" as const; #flags = new FrequencySet(); diff --git a/workspaces/scanner/src/extractors/probes/LicensesExtractor.class.ts b/workspaces/scanner/src/extractors/probes/LicensesExtractor.class.ts index 7d46d713..5ccc31d6 100644 --- a/workspaces/scanner/src/extractors/probes/LicensesExtractor.class.ts +++ b/workspaces/scanner/src/extractors/probes/LicensesExtractor.class.ts @@ -4,14 +4,14 @@ import type { } from "../payload.js"; import type { DependencyVersion } from "../../types.js"; -export type LicensesExtractorResult = { +export type LicensesResult = { licenses: Record; }; -export class LicensesExtractor implements ManifestProbeExtractor { +export class Licenses implements ManifestProbeExtractor { level = "manifest" as const; - #licenses: LicensesExtractorResult["licenses"] = Object.create(null); + #licenses: LicensesResult["licenses"] = Object.create(null); next( _: string, diff --git a/workspaces/scanner/src/extractors/probes/SizeExtractor.class.ts b/workspaces/scanner/src/extractors/probes/SizeExtractor.class.ts index f438602b..3986e0e1 100644 --- a/workspaces/scanner/src/extractors/probes/SizeExtractor.class.ts +++ b/workspaces/scanner/src/extractors/probes/SizeExtractor.class.ts @@ -8,7 +8,7 @@ import type { } from "../payload.js"; import type { DependencyVersion } from "../../types.js"; -export type SizeExtractorResult = { +export type SizeResult = { size: { all: string; internal: string; @@ -16,11 +16,11 @@ export type SizeExtractorResult = { }; }; -export interface SizeExtractorOptions { +export interface SizeOptions { organizationPrefix?: string; } -export class SizeExtractor implements ManifestProbeExtractor { +export class Size implements ManifestProbeExtractor { level = "manifest" as const; #size = { @@ -31,7 +31,7 @@ export class SizeExtractor implements ManifestProbeExtractor { +export class Vulnerabilities implements PackumentProbeExtractor { level = "packument" as const; #vulnerabilities: StandardVulnerability[] = []; diff --git a/workspaces/scanner/src/extractors/probes/WarningsExtractor.class.ts b/workspaces/scanner/src/extractors/probes/WarningsExtractor.class.ts index 21df64dc..5c88a3e3 100644 --- a/workspaces/scanner/src/extractors/probes/WarningsExtractor.class.ts +++ b/workspaces/scanner/src/extractors/probes/WarningsExtractor.class.ts @@ -13,7 +13,7 @@ import type { } from "../payload.js"; import type { DependencyVersion } from "../../types.js"; -export type WarningsExtractorResult = { +export type WarningsResult = { warnings: { count: number; groups: Record[]>; @@ -21,14 +21,14 @@ export type WarningsExtractorResult = { }; }; -export interface WarningsExtractorOptions { +export interface WarningsOptions { /** * @default true */ useSpecAsKey?: boolean; } -export class WarningsExtractor implements ManifestProbeExtractor { +export class Warnings implements ManifestProbeExtractor { level = "manifest" as const; #warnings: Record[]> = Object.create(null); @@ -37,7 +37,7 @@ export class WarningsExtractor implements ManifestProbeExtractor { - describe("ContactExtractor", () => { + describe("Contacts", () => { it("should extract Express.js contacts (Author, Maintainers ...)", () => { const extractor = new Extractors.Payload( expressNodesecurePayload, [ - new Extractors.Probes.ContactExtractor() + new Extractors.Probes.Contacts() ] ); @@ -82,12 +84,12 @@ describe("Extractors.Probes", () => { }); }); - describe("LicensesExtractor", () => { + describe("Licenses", () => { it("should extract Express.js licenses", () => { const extractor = new Extractors.Payload( expressNodesecurePayload, [ - new Extractors.Probes.LicensesExtractor() + new Extractors.Probes.Licenses() ] ); @@ -106,7 +108,7 @@ describe("Extractors.Probes", () => { const extractor = new Extractors.Payload( expressNodesecurePayload, [ - new Extractors.Probes.SizeExtractor() + new Extractors.Probes.Size() ] ); @@ -129,12 +131,12 @@ describe("Extractors.Probes", () => { }); }); - describe("WarningsExtractor", () => { + describe("Warnings", () => { it("should extract strnum warnings", () => { const extractor = new Extractors.Payload( strnumNodesecurePayload, [ - new Extractors.Probes.WarningsExtractor() + new Extractors.Probes.Warnings() ] ); @@ -163,7 +165,7 @@ describe("Extractors.Probes", () => { const extractor = new Extractors.Payload( strnumNodesecurePayload, [ - new Extractors.Probes.WarningsExtractor({ + new Extractors.Probes.Warnings({ useSpecAsKey: false }) ] @@ -179,12 +181,12 @@ describe("Extractors.Probes", () => { }); }); - describe("FlagsExtractor", () => { + describe("Flags", () => { it("should extract strnum flags", () => { const extractor = new Extractors.Payload( strnumNodesecurePayload, [ - new Extractors.Probes.FlagsExtractor() + new Extractors.Probes.Flags() ] ); @@ -203,7 +205,7 @@ describe("Extractors.Probes", () => { }); }); - describe("VulnerabilitiesExtractor", () => { + describe("Vulnerabilities", () => { it("should extract strnum warnings", () => { const fakePayload: any = { id: "random-id", @@ -221,7 +223,7 @@ describe("Extractors.Probes", () => { const extractor = new Extractors.Payload( fakePayload, [ - new Extractors.Probes.VulnerabilitiesExtractor() + new Extractors.Probes.Vulnerabilities() ] ); @@ -238,9 +240,9 @@ describe("Extractors.Probes", () => { const extractor = new Extractors.Payload( expressNodesecurePayload, [ - new Extractors.Probes.SizeExtractor(), - new Extractors.Probes.ContactExtractor(), - new Extractors.Probes.LicensesExtractor() + new Extractors.Probes.Size(), + new Extractors.Probes.Contacts(), + new Extractors.Probes.Licenses() ] ); @@ -255,18 +257,16 @@ 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; +describe("Extractors.Payload events", () => { + type ManifestEvent = Parameters; + type PackumentEvent = Parameters; + it("should emits packument and manifest events", () => { const extractor = new Extractors.Payload( expressNodesecurePayload, [ - licensesExtractor, - vulnerabilitiesExtractor + new Extractors.Probes.Licenses(), + new Extractors.Probes.Vulnerabilities() ] ); @@ -296,3 +296,100 @@ describe("Events", () => { assert.deepEqual(manifestEvents, expectedManifestEvents); }); }); + +describe("Extractors.Callbacks", () => { + it("should extract name and versions for all packages", () => { + const packages = new Map(); + + const extractor = new Extractors.Payload( + expressNodesecurePayload, + [ + Extractors.Callbacks.packument((name) => { + if (!packages.has(name)) { + packages.set(name, []); + } + }), + Extractors.Callbacks.manifest((spec, _, parent) => { + if (packages.has(parent.name)) { + packages.get(parent.name)!.push(spec); + } + }) + ] + ); + + extractor.extract(); + + assert.deepEqual( + Object.fromEntries(packages), + { + etag: ["1.8.1"], + setprototypeof: ["1.2.0"], + methods: ["1.1.2"], + depd: ["2.0.0"], + fresh: ["0.5.2"], + vary: ["1.1.2"], + "escape-html": ["1.0.3"], + encodeurl: ["2.0.0", "1.0.2"], + statuses: ["2.0.1"], + "content-type": ["1.0.5"], + "safe-buffer": ["5.2.1"], + "range-parser": ["1.2.1"], + "utils-merge": ["1.0.1"], + "array-flatten": ["1.1.1"], + cookie: ["0.7.1"], + "cookie-signature": ["1.0.6"], + parseurl: ["1.3.3"], + "merge-descriptors": ["1.0.3"], + "path-to-regexp": ["0.1.12"], + "content-disposition": ["0.5.4"], + "ee-first": ["1.1.1"], + "on-finished": ["2.4.1"], + negotiator: ["0.6.3"], + accepts: ["1.3.8"], + forwarded: ["0.2.0"], + ms: ["2.1.3", "2.0.0"], + inherits: ["2.0.4"], + debug: ["2.6.9"], + "ipaddr.js": ["1.9.1"], + "proxy-addr": ["2.0.7"], + qs: ["6.13.0"], + mime: ["1.6.0"], + "mime-db": ["1.52.0"], + "mime-types": ["2.1.35"], + bytes: ["3.1.2"], + unpipe: ["1.0.0"], + toidentifier: ["1.0.1"], + "http-errors": ["2.0.0"], + destroy: ["1.2.0"], + "media-typer": ["0.3.0"], + "type-is": ["1.6.18"], + "es-errors": ["1.3.0"], + send: ["0.19.0"], + finalhandler: ["1.3.1"], + "object-inspect": ["1.13.3"], + "serve-static": ["1.16.2"], + "side-channel-list": ["1.0.0"], + "safer-buffer": ["2.1.2"], + "iconv-lite": ["0.4.24"], + "raw-body": ["2.5.2"], + "body-parser": ["1.20.3"], + "function-bind": ["1.1.2"], + "call-bind-apply-helpers": ["1.0.1"], + "es-define-property": ["1.0.1"], + "es-object-atoms": ["1.1.1"], + gopd: ["1.2.0"], + "dunder-proto": ["1.0.1"], + "get-proto": ["1.0.1"], + "has-symbols": ["1.1.0"], + hasown: ["2.0.2"], + "math-intrinsics": ["1.1.0"], + "get-intrinsic": ["1.2.7"], + "call-bound": ["1.0.3"], + "side-channel-map": ["1.0.1"], + "side-channel-weakmap": ["1.0.2"], + "side-channel": ["1.1.0"], + express: ["4.21.2"] + } + ); + }); +});