diff --git a/.changeset/tall-kings-lie.md b/.changeset/tall-kings-lie.md new file mode 100644 index 00000000..ac8e16da --- /dev/null +++ b/.changeset/tall-kings-lie.md @@ -0,0 +1,5 @@ +--- +"@nodesecure/scanner": minor +--- + +Implement a new extraction probe for warnings diff --git a/workspaces/scanner/src/extractors/probes/WarningsExtractor.class.ts b/workspaces/scanner/src/extractors/probes/WarningsExtractor.class.ts new file mode 100644 index 00000000..1f5c6406 --- /dev/null +++ b/workspaces/scanner/src/extractors/probes/WarningsExtractor.class.ts @@ -0,0 +1,65 @@ +// Import Third-party Dependencies +import type { WarningDefault, Warning } from "@nodesecure/js-x-ray"; + +// Import Internal Dependencies +import type { + ManifestProbeExtractor, + ProbeExtractorManifestParent +} from "../payload.js"; +import type { DependencyVersion } from "../../types.js"; + +export type WarningsExtractorResult = { + warnings: Record[]>; + count: number; +}; + +export interface WarningsExtractorOptions { + /** + * @default true + */ + useSpecAsKey?: boolean; +} + +export class WarningsExtractor implements ManifestProbeExtractor { + level = "manifest" as const; + + #warnings: Record[]> = Object.create(null); + #count = 0; + #useSpecAsKey: boolean; + + constructor( + options: WarningsExtractorOptions = {} + ) { + this.#useSpecAsKey = options.useSpecAsKey ?? true; + } + + next( + version: string, + depVersion: DependencyVersion, + parent: ProbeExtractorManifestParent + ) { + const { warnings } = depVersion; + if (warnings.length === 0) { + return; + } + + this.#count += warnings.length; + const key = this.#useSpecAsKey ? + `${parent.name}@${version}` : + parent.name; + + if (key in this.#warnings) { + this.#warnings[key].push(...warnings); + } + else { + this.#warnings[key] = [...warnings]; + } + } + + done() { + return { + count: this.#count, + warnings: this.#warnings + }; + } +} diff --git a/workspaces/scanner/src/extractors/probes/index.ts b/workspaces/scanner/src/extractors/probes/index.ts index 2aeccfc9..fbc01c08 100644 --- a/workspaces/scanner/src/extractors/probes/index.ts +++ b/workspaces/scanner/src/extractors/probes/index.ts @@ -1,3 +1,4 @@ export * from "./SizeExtractor.class.js"; export * from "./LicensesExtractor.class.js"; export * from "./ContactExtractor.class.js"; +export * from "./WarningsExtractor.class.js"; diff --git a/workspaces/scanner/test/extractors/payload.spec.ts b/workspaces/scanner/test/extractors/payload.spec.ts index d7fca3ba..cf349898 100644 --- a/workspaces/scanner/test/extractors/payload.spec.ts +++ b/workspaces/scanner/test/extractors/payload.spec.ts @@ -18,6 +18,10 @@ const expressNodesecurePayload = JSON.parse(fs.readFileSync( new URL(path.join("..", kFixturePath, "express.json"), import.meta.url), "utf8" )) as Payload; +const strnumNodesecurePayload = JSON.parse(fs.readFileSync( + new URL(path.join("..", kFixturePath, "strnum.json"), import.meta.url), + "utf8" +)) as Payload; describe("Extractors.Probes", () => { describe("ContactExtractor", () => { @@ -125,6 +129,49 @@ describe("Extractors.Probes", () => { }); }); + describe("WarningsExtractor", () => { + it("should extract strnum warnings", () => { + const extractor = new Extractors.Payload( + strnumNodesecurePayload, + [ + new Extractors.Probes.WarningsExtractor() + ] + ); + + const { + count, + warnings + } = extractor.extractAndMerge(); + + assert.strictEqual(count, 3); + const keys = Object.keys(warnings); + assert.deepEqual(keys, ["strnum@1.1.2"]); + + const kinds = warnings["strnum@1.1.2"].map((warning) => warning.kind); + assert.deepEqual(kinds, ["unsafe-regex", "unsafe-regex", "encoded-literal"]); + }); + + it("should extract strnum warnings with options useSpecAsKey: false", () => { + const extractor = new Extractors.Payload( + strnumNodesecurePayload, + [ + new Extractors.Probes.WarningsExtractor({ + useSpecAsKey: false + }) + ] + ); + + const { + count, + warnings + } = extractor.extractAndMerge(); + + assert.strictEqual(count, 3); + const keys = Object.keys(warnings); + assert.deepEqual(keys, ["strnum"]); + }); + }); + it("should extract data with multiple extractors in once", () => { const extractor = new Extractors.Payload( expressNodesecurePayload, diff --git a/workspaces/scanner/test/fixtures/extractors/strnum.json b/workspaces/scanner/test/fixtures/extractors/strnum.json new file mode 100644 index 00000000..d94150ff --- /dev/null +++ b/workspaces/scanner/test/fixtures/extractors/strnum.json @@ -0,0 +1,204 @@ +{ + "id": "54mMPc", + "rootDependencyName": "strnum", + "scannerVersion": "6.4.0", + "vulnerabilityStrategy": "none", + "warnings": [], + "highlighted": { + "contacts": [] + }, + "dependencies": { + "strnum": { + "versions": { + "1.1.2": { + "id": 0, + "usedBy": {}, + "isDevDependency": false, + "existOnRemoteRegistry": true, + "flags": [ + "hasWarnings", + "isOutdated", + "hasManyPublishers" + ], + "warnings": [ + { + "kind": "unsafe-regex", + "location": [ + [ + 2, + 17 + ], + [ + 2, + 53 + ] + ], + "source": "JS-X-Ray", + "value": "^([\\-\\+])?(0*)([0-9]*(\\.[0-9]*)?)$", + "i18n": "sast_warnings.unsafe_regex", + "severity": "Warning", + "file": "strnum.js" + }, + { + "kind": "unsafe-regex", + "location": [ + [ + 29, + 42 + ], + [ + 29, + 93 + ] + ], + "source": "JS-X-Ray", + "value": "^([-\\+])?(0*)([0-9]*(\\.[0-9]*)?[eE][-\\+]?[0-9]+)$", + "i18n": "sast_warnings.unsafe_regex", + "severity": "Warning", + "file": "strnum.js" + }, + { + "kind": "encoded-literal", + "value": "e89794659669cb7bb967db73a7ea6889c3891727", + "location": [ + [ + [ + 9, + 24 + ], + [ + 9, + 66 + ] + ], + [ + [ + 9, + 77 + ], + [ + 9, + 119 + ] + ] + ], + "source": "JS-X-Ray", + "i18n": "sast_warnings.encoded_literal", + "severity": "Information", + "file": "strnum.test.js" + } + ], + "dependencyCount": 0, + "gitUrl": null, + "alias": {}, + "description": "Parse String to Number based on configuration", + "size": 18505, + "author": { + "name": "Amit Gupta", + "url": "https://amitkumargupta.work/" + }, + "scripts": { + "test": "jasmine strnum.test.js" + }, + "licenses": [ + { + "licenses": { + "MIT": "https://spdx.org/licenses/MIT.html#licenseText" + }, + "spdx": { + "osi": true, + "fsf": true, + "fsfAndOsi": true, + "includesDeprecated": false + }, + "fileName": "package.json" + }, + { + "licenses": { + "MIT": "https://spdx.org/licenses/MIT.html#licenseText" + }, + "spdx": { + "osi": true, + "fsf": true, + "fsfAndOsi": true, + "includesDeprecated": false + }, + "fileName": "LICENSE" + } + ], + "uniqueLicenseIds": [ + "MIT" + ], + "composition": { + "extensions": [ + ".md", + "", + ".json", + ".js" + ], + "files": [ + "CHANGELOG.md", + "LICENSE", + "README.md", + "package.json", + "strnum.js", + "strnum.test.js" + ], + "minified": [], + "unused": [], + "missing": [], + "required_files": [ + "strnum.js" + ], + "required_nodejs": [], + "required_thirdparty": [], + "required_subpath": {} + }, + "repository": { + "type": "git", + "url": "https://github.com/NaturalIntelligence/strnum" + }, + "integrity": "c6cfe9d4c17b06667b050142e0bf8b21f21b6104", + "links": { + "npm": "https://www.npmjs.com/package/strnum/v/1.1.2", + "homepage": "https://github.com/NaturalIntelligence/strnum#readme", + "repository": "https://github.com/NaturalIntelligence/strnum" + } + } + }, + "vulnerabilities": [], + "metadata": { + "author": { + "name": "Amit Gupta", + "url": "https://amitkumargupta.work/" + }, + "homepage": "https://github.com/NaturalIntelligence/strnum#readme", + "publishedCount": 16, + "lastVersion": "2.1.1", + "lastUpdateAt": "2025-05-15T06:53:57.067Z", + "hasReceivedUpdateInOneYear": true, + "hasManyPublishers": true, + "hasChangedAuthor": false, + "maintainers": [ + { + "name": "amitgupta", + "email": "amitgupta.gwl@gmail.com", + "npmAvatar": "/npm-avatar/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdmF0YXJVUkwiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci8zNzA2MDMwMjM4NmFhZjQxMWQ2OTU1OTY4MDU2MjIzND9zaXplPTUwJmRlZmF1bHQ9cmV0cm8ifQ.RIVjeGPMfLAcKEPB9PTigtkKQacb-UKit5unRVSmfGM" + } + ], + "publishers": [ + { + "name": "amitgupta", + "email": "amitgupta.gwl@gmail.com", + "version": "2.1.1", + "at": "2025-05-15T06:53:57.067Z", + "npmAvatar": "/npm-avatar/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdmF0YXJVUkwiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci8zNzA2MDMwMjM4NmFhZjQxMWQ2OTU1OTY4MDU2MjIzND9zaXplPTUwJmRlZmF1bHQ9cmV0cm8ifQ.RIVjeGPMfLAcKEPB9PTigtkKQacb-UKit5unRVSmfGM" + } + ], + "integrity": { + "1.1.2": "c6cfe9d4c17b06667b050142e0bf8b21f21b6104" + } + } + } + } +}