diff --git a/bin/index.js b/bin/index.js index ed27e4f3..fa3577fd 100755 --- a/bin/index.js +++ b/bin/index.js @@ -152,6 +152,7 @@ function defaultScannerCommand(name, options = {}) { .option("-d, --depth", i18n.getTokenSync("cli.commands.option_depth"), Infinity) .option("--silent", i18n.getTokenSync("cli.commands.option_silent"), false) .option("-c, --contacts", i18n.getTokenSync("cli.commands.option_contacts"), []) + .option("-p, --packages", i18n.getTokenSync("cli.commands.option_packages"), []) .option("--verbose", i18n.getTokenSync("cli.commands.option_verbose"), false); if (includeOutput) { diff --git a/docs/cli/auto.md b/docs/cli/auto.md index b1cc61b7..824509a2 100644 --- a/docs/cli/auto.md +++ b/docs/cli/auto.md @@ -28,4 +28,6 @@ $ nsecure auto --keep | `--vulnerabilityStrategy` | `-s` | github-advisory | Strategy used to fetch package vulnerabilities (see Vulnera [available strategy](https://github.com/NodeSecure/vulnera?tab=readme-ov-file#available-strategy)). | | `--keep` | `-k` | `false` | Preserve JSON payload after execution. | | `--developer` | | `false` | Launch the server in developer mode, enabling automatic refresh on HTML/CSS/JS changes. | -| `--contacts` | `-c` | `[]` | List of contacts to highlight. | `--verbose` | | `false` | Sets cli log level to verbose, causing the CLI to output more detailed logs. | +| `--contacts` | `-c` | `[]` | List of contacts to highlight. | +| `--packages` | `-p` | `[]` | List of packages to highlight. | +`--verbose` | | `false` | Sets cli log level to verbose, causing the CLI to output more detailed logs. | diff --git a/docs/cli/cwd.md b/docs/cli/cwd.md index 5d4888de..9def6099 100644 --- a/docs/cli/cwd.md +++ b/docs/cli/cwd.md @@ -18,4 +18,6 @@ $ nsecure cwd [options] | `--silent` | | `false` | Suppress console output, making execution silent. | | `--output` | `-o` | `nsecure-result` | Specify the output file for the results. | | `--vulnerabilityStrategy` | `-s` | github-advisory | Strategy used to fetch package vulnerabilities (see Vulnera [available strategy](https://github.com/NodeSecure/vulnera?tab=readme-ov-file#available-strategy)). | -| `--contacts` | `-c` | `[]` | List of contacts to highlight. | `--verbose` | | `false` | Sets cli log level to verbose, causing the CLI to output more detailed logs. | +| `--contacts` | `-c` | `[]` | List of contacts to highlight. | +| `--packages` | `-p` | `[]` | List of packages to highlight. | +`--verbose` | | `false` | Sets cli log level to verbose, causing the CLI to output more detailed logs. | diff --git a/docs/cli/from.md b/docs/cli/from.md index 7613cd14..b2676d74 100644 --- a/docs/cli/from.md +++ b/docs/cli/from.md @@ -24,4 +24,6 @@ $ nsecure from express@3.0.0 -o express-report | `--silent` | | `false` | Suppress console output, making execution silent. | | `--output` | `-o` | `nsecure-result` | Specify the output file for the results. | | `--vulnerabilityStrategy` | `-s` | github-advisory | Strategy used to fetch package vulnerabilities (see Vulnera [available strategy](https://github.com/NodeSecure/vulnera?tab=readme-ov-file#available-strategy)). | -| `--contacts` | `-c` | `[]` | List of contacts to highlight. | `--verbose` | | `false` | Sets cli log level to verbose, causing the CLI to output more detailed logs. | +| `--contacts` | `-c` | `[]` | List of contacts to highlight. | +| `--packages` | `-p` | `[]` | List of packages to highlight. | +`--verbose` | | `false` | Sets cli log level to verbose, causing the CLI to output more detailed logs. | diff --git a/i18n/arabic.js b/i18n/arabic.js index 2ad93550..ae8889dd 100644 --- a/i18n/arabic.js +++ b/i18n/arabic.js @@ -22,6 +22,7 @@ const cli = { option_output: "اسم ملف JSON الناتج", option_silent: "تفعيل الوضع الصامت الذي يعطل مؤشرات CLI", option_contacts: "قائمة جهات الاتصال للتمييز", + option_packages: "قائمة الحزم للتمييز", option_verbose: "ضبط مستوى الـ log الخاص بالـ CLI على verbose، مما يجعل الـ CLI يولّد logs أكثر تفصيلاً.", strategy: "مصدر الثغرات للاستخدام", cwd: { diff --git a/i18n/english.js b/i18n/english.js index 2cb7ffe0..01f5eaa8 100644 --- a/i18n/english.js +++ b/i18n/english.js @@ -27,6 +27,7 @@ const cli = { option_output: "Json file output name", option_silent: "enable silent mode which disable CLI spinners", option_contacts: "List of contacts to hightlight", + option_packages: "List of packages to highlight", option_verbose: "Sets cli log level to verbose, causing the CLI to output more detailed logs.", strategy: "Vulnerabilities source to use", cwd: { diff --git a/i18n/french.js b/i18n/french.js index 688baee9..95cf6d33 100644 --- a/i18n/french.js +++ b/i18n/french.js @@ -27,6 +27,7 @@ const cli = { option_output: "Nom de sortie du fichier json", option_silent: "Activer le mode silencieux qui désactive les spinners du CLI", option_contacts: "Liste des contacts à mettre en évidence", + option_packages: "Liste des packages à mettre en évidence", option_verbose: "Définir le niveau de log CLI à verbeux, ce qui amènera la CLI à générer des logs plus détaillés.", strategy: "Source de vulnérabilités à utiliser", cwd: { diff --git a/i18n/turkish.js b/i18n/turkish.js index 7e438ec8..4365a215 100644 --- a/i18n/turkish.js +++ b/i18n/turkish.js @@ -24,6 +24,7 @@ const cli = { option_output: "JSON dosyası çıktı adı", option_silent: "CLI döndürücülerini devre dışı bırakan sessiz modu etkinleştir", option_contacts: "Vurgulanacak kişilerin listesi", + option_packages: "Vurgulanacak paketlerin listesi", option_verbose: "CLI'nin log seviyesini verbose olarak ayarlar, bu da CLI'nin daha ayrıntılı loglar üretmesine neden olur.", strategy: "Kullanılacak güvenlik açığı kaynağı", cwd: { diff --git a/src/commands/parsers/packages.js b/src/commands/parsers/packages.js new file mode 100644 index 00000000..992ec99c --- /dev/null +++ b/src/commands/parsers/packages.js @@ -0,0 +1,34 @@ +// Import Third-party Dependencies +import { parseNpmSpec } from "@nodesecure/mama"; + +/** + * Parse a list of CLI package strings into the expected HighlightPackages format expected + * by @nodesecure/scanner: `string[] | Record`. + * + * Each input string can be: + * - "lodash" → plain name, no version constraint + * - "lodash@^4.0.0" → name with a semver range + * - "@scope/pkg" → scoped package, no version constraint + * - "@scope/pkg@^1.0.0" → scoped package with a semver range + * + * When none of the entries carry a version constraint the function returns a plain `string[]`. + * If at least one entry has a version constraint the function returns a `Record`; + * Entries without a constraint are mapped to '*' + * + * @param {string | string[]} input + * @returns {string[] | Record} + */ +export function parsePackages(input) { + const items = Array.isArray(input) ? input : [input]; + const parsed = items.map(parseNpmSpec); + + const hasVersionConstraints = parsed.some(({ semver }) => semver !== null); + + if (hasVersionConstraints) { + return Object.fromEntries( + parsed.map(({ name, semver }) => [name, semver ?? "*"]) + ); + } + + return parsed.map(({ name }) => name); +} diff --git a/src/commands/scanner.js b/src/commands/scanner.js index e25de274..a3fe9f60 100644 --- a/src/commands/scanner.js +++ b/src/commands/scanner.js @@ -20,14 +20,16 @@ import { formatMs } from "./loggers/logger.js"; import { parseContacts } from "./parsers/contacts.js"; +import { parsePackages } from "./parsers/packages.js"; export async function auto(spec, options) { const { keep, ...commandOptions } = options; - const optionsWithContacts = { + const optionsWithHighlight = { ...commandOptions, highlight: { - contacts: parseContacts(options.contacts) + contacts: parseContacts(options.contacts), + packages: parsePackages(options.packages ?? []) } }; @@ -36,8 +38,8 @@ export async function auto(spec, options) { const payloadFile = await ( typeof spec === "string" ? - from(spec, optionsWithContacts, cache) : - cwd(optionsWithContacts, cache) + from(spec, optionsWithHighlight, cache) : + cwd(optionsWithHighlight, cache) ); try { if (payloadFile !== null) { @@ -75,6 +77,7 @@ export async function cwd(options, cache) { vulnerabilityStrategy, silent, contacts, + packages, verbose } = options; @@ -86,7 +89,8 @@ export async function cwd(options, cache) { fullLockMode: full, vulnerabilityStrategy, highlight: { - contacts: parseContacts(contacts) + contacts: parseContacts(contacts), + packages: parsePackages(packages) }, isVerbose: verbose, workers: true, @@ -118,6 +122,7 @@ export async function from(spec, options, cache) { output, silent, contacts, + packages, vulnerabilityStrategy, verbose } = options; @@ -128,7 +133,8 @@ export async function from(spec, options, cache) { maxDepth, vulnerabilityStrategy, highlight: { - contacts: parseContacts(contacts) + contacts: parseContacts(contacts), + packages: parsePackages(packages) }, isVerbose: verbose, workers: true, diff --git a/test/commands/parsers/packages.test.js b/test/commands/parsers/packages.test.js new file mode 100644 index 00000000..6651a8e0 --- /dev/null +++ b/test/commands/parsers/packages.test.js @@ -0,0 +1,56 @@ +// Import Node.js Dependencies +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; + +// Import Internal Dependencies +import { parsePackages } from "../../../src/commands/parsers/packages.js"; + +describe("packages parser", () => { + describe("returns string[] when no version constraints", () => { + it("should parse a single plain package name", () => { + assert.deepEqual(parsePackages("lodash"), ["lodash"]); + }); + + it("should parse a single scoped package with no version", () => { + assert.deepEqual(parsePackages("@scope/pkg"), ["@scope/pkg"]); + }); + + it("should parse multiple plain packages with no versions", () => { + assert.deepEqual(parsePackages(["lodash", "express"]), ["lodash", "express"]); + }); + }); + + describe("returns Record when at least one entry has a version constraint", () => { + it("should parse a package with a semver range", () => { + assert.deepEqual(parsePackages("lodash@^4.0.0"), { lodash: "^4.0.0" }); + }); + + it("should parse a scoped package with a semver range", () => { + assert.deepEqual(parsePackages("@scope/pkg@^1.0.0"), { "@scope/pkg": "^1.0.0" }); + }); + + it("should map entries without a version to '*' when mixed with versioned entries", () => { + assert.deepEqual( + parsePackages(["lodash", "express@^4.0.0"]), + { lodash: "*", express: "^4.0.0" } + ); + }); + + it("should parse multiple packages all with version constraints", () => { + assert.deepEqual( + parsePackages(["lodash@^4.0.0", "express@^4.18.0"]), + { lodash: "^4.0.0", express: "^4.18.0" } + ); + }); + }); + + describe("edge cases", () => { + it("should return an empty array for an empty array input", () => { + assert.deepEqual(parsePackages([]), []); + }); + + it("should treat a scoped name with no slash and no version as a plain package name", () => { + assert.deepEqual(parsePackages("@scope"), ["@scope"]); + }); + }); +});