From 9b8f762f025a1bab2f4860dc14058d70bbf74599 Mon Sep 17 00:00:00 2001 From: cgombauld Date: Sun, 8 Jun 2025 10:28:20 +0200 Subject: [PATCH 1/8] feat(contacts): implemented parser --- bin/index.js | 3 +- i18n/english.js | 8 ++ i18n/french.js | 8 ++ .../views/home/maintainers/maintainers.js | 14 +++- src/commands/parsers/contacts.js | 33 ++++++++ src/commands/scanner.js | 30 +++++-- test/commands/parsers/contacts.test.js | 78 +++++++++++++++++++ 7 files changed, 165 insertions(+), 9 deletions(-) create mode 100644 src/commands/parsers/contacts.js create mode 100644 test/commands/parsers/contacts.test.js diff --git a/bin/index.js b/bin/index.js index 99911d5c..552cc4fd 100755 --- a/bin/index.js +++ b/bin/index.js @@ -135,7 +135,8 @@ function defaultScannerCommand(name, options = {}) { const cmd = prog.command(name) .option("-d, --depth", i18n.getTokenSync("cli.commands.option_depth"), Infinity) - .option("--silent", i18n.getTokenSync("cli.commands.option_silent"), false); + .option("--silent", i18n.getTokenSync("cli.commands.option_silent"), false) + .option("-c, --contacts", i18n.getTokenSync("cli.commands.option_contacts"), "[]"); if (includeOutput) { cmd.option("-o, --output", i18n.getTokenSync("cli.commands.option_output"), "nsecure-result"); diff --git a/i18n/english.js b/i18n/english.js index f2819ada..d242ea5f 100644 --- a/i18n/english.js +++ b/i18n/english.js @@ -14,6 +14,7 @@ const cli = { option_depth: "Maximum dependencies depth to fetch", option_output: "Json file output name", option_silent: "enable silent mode which disable CLI spinners", + option_contacts: "List of contacts to hightlight", strategy: "Vulnerabilities source to use", cwd: { desc: "Run security analysis on the current working dir", @@ -80,6 +81,13 @@ const cli = { startHttp: { invalidScannerVersion: tS`the payload has been scanned with version '${0}' and do not satisfies the required CLI range '${1}'`, regenerate: "please re-generate a new JSON payload using the CLI" + }, + errors: { + contacts: { + should_be_valid_json: tS`Contacts: ${0}`, + should_be_array: "Contacts should be an array", + should_be_defined: tS`Contact at index ${0} should not be null` + } } }; diff --git a/i18n/french.js b/i18n/french.js index b41348fa..15d099d0 100644 --- a/i18n/french.js +++ b/i18n/french.js @@ -14,6 +14,7 @@ const cli = { option_depth: "Niveau de profondeur de dépendances maximum à aller chercher", 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", strategy: "Source de vulnérabilités à utiliser", cwd: { desc: "Démarre une analyse de sécurité sur le dossier courant", @@ -80,6 +81,13 @@ const cli = { startHttp: { invalidScannerVersion: tS`le fichier d'analyse correspond à la version '${0}' du scanner et ne satisfait pas la range '${1}' attendu par la CLI`, regenerate: "veuillez re-générer un nouveau fichier d'analyse JSON en utilisant votre CLI" + }, + errors: { + contacts: { + should_be_valid_json: tS`Contacts: ${0}`, + should_be_array: "Contacts doit etre un array", + should_be_defined: tS`Contact à index ${0} ne doit pas etre null` + } } }; diff --git a/public/components/views/home/maintainers/maintainers.js b/public/components/views/home/maintainers/maintainers.js index e2379897..c80a35fe 100644 --- a/public/components/views/home/maintainers/maintainers.js +++ b/public/components/views/home/maintainers/maintainers.js @@ -26,14 +26,24 @@ export class Maintainers { } render() { - const authors = [...this.secureDataSet.authors.entries()] - .sort((left, right) => right[1].packages.size - left[1].packages.size); + const authors = this.#highlightContacts([...this.secureDataSet.authors.entries()] + .sort((left, right) => right[1].packages.size - left[1].packages.size)); document.getElementById("authors-count").innerHTML = authors.length; document.querySelector(".home--maintainers") .appendChild(this.generate(authors)); } + #highlightContacts(authors) { + const highlightedContacts = new Set(this.secureDataSet.data.highlighted.contacts + .map(({ name }) => name)); + + const highlightedAuthors = authors.filter(([name]) => highlightedContacts.has(name)); + const authorsRest = authors.filter(([name]) => !highlightedContacts.has(name)); + + return [...highlightedAuthors, ...authorsRest]; + } + generate(authors) { const fragment = document.createDocumentFragment(); const hideItems = authors.length > this.maximumMaintainers; diff --git a/src/commands/parsers/contacts.js b/src/commands/parsers/contacts.js new file mode 100644 index 00000000..9e3cbb3e --- /dev/null +++ b/src/commands/parsers/contacts.js @@ -0,0 +1,33 @@ +export function createContactsParser({ logError, exit }) { + return (json) => { + const contacts = parseContacts({ json, logError, exit }); + if (!Array.isArray(contacts)) { + logError("cli.errors.contacts.should_be_array"); + exit(); + } + let hasError = false; + contacts.forEach((contact, i) => { + if (!contact) { + hasError = true; + logError("cli.errors.contacts.should_be_defined", i); + } + }); + if (hasError) { + exit(); + } + + return contacts; + }; +} + +function parseContacts({ json, logError, exit }) { + try { + return JSON.parse(json); + } + catch (err) { + logError("cli.errors.contacts.should_be_valid_json", err.message); + exit(); + + return null; + } +} diff --git a/src/commands/scanner.js b/src/commands/scanner.js index 89d0ab40..5642cc91 100644 --- a/src/commands/scanner.js +++ b/src/commands/scanner.js @@ -14,14 +14,28 @@ import * as Scanner from "@nodesecure/scanner"; // Import Internal Dependencies import * as http from "./http.js"; import { appCache } from "../cache.js"; +import { createContactsParser } from "./parsers/contacts.js"; + +const parseContacts = createContactsParser({ + logError: (tokenName, param) => console.log(kleur.red().bold(param ? i18n.getTokenSync(tokenName, param) + : i18n.getTokenSync(tokenName))), + exit: () => process.exit() +}); export async function auto(spec, options) { const { keep, ...commandOptions } = options; + const optionsWithContacts = { + ...commandOptions, + highlight: { + contacts: parseContacts(options.contacts) + } + }; + const payloadFile = await ( typeof spec === "string" ? - from(spec, commandOptions) : - cwd(commandOptions) + from(spec, optionsWithContacts) : + cwd(optionsWithContacts) ); try { if (payloadFile !== null) { @@ -55,12 +69,14 @@ export async function cwd(options) { nolock, full, vulnerabilityStrategy, - silent + silent, + contacts } = options; const payload = await Scanner.cwd( process.cwd(), - { maxDepth, usePackageLock: !nolock, fullLockMode: full, vulnerabilityStrategy }, + { maxDepth, usePackageLock: !nolock, fullLockMode: full, vulnerabilityStrategy, highlight: + { contacts: parseContacts(contacts) } }, initLogger(void 0, !silent) ); @@ -68,11 +84,13 @@ export async function cwd(options) { } export async function from(spec, options) { - const { depth: maxDepth = Infinity, output, silent } = options; + const { depth: maxDepth = Infinity, output, silent, contacts } = options; const payload = await Scanner.from( spec, - { maxDepth }, + { maxDepth, highlight: { + contacts: parseContacts(contacts) + } }, initLogger(spec, !silent) ); diff --git a/test/commands/parsers/contacts.test.js b/test/commands/parsers/contacts.test.js new file mode 100644 index 00000000..18e49000 --- /dev/null +++ b/test/commands/parsers/contacts.test.js @@ -0,0 +1,78 @@ +// Import Node.js Dependencies +import { it, describe, beforeEach } from "node:test"; +import assert from "node:assert/strict"; + +// Import Internal Dependencies +import { createContactsParser } from "../../../src/commands/parsers/contacts.js"; + +let errors = []; + +function exit() { + throw new Error("process exited"); +} + +beforeEach(() => { + errors = []; +}); + +function logError(token, param) { + if (param) { + errors.push(`${token} ${param}`); + } + else { + errors.push(token); + } +} + +describe("contacts parser", () => { + it("should successfully parse the contacts to highlight", () => { + const parseContacts = createContactsParser({ + logError, + exit + }); + + const contactsJson = "[{\"name\": \"contact1\"},{\"name\":\"contact2\",\"url\":\"url2\",\"email\":\"email2@gmail.com\"}]"; + const result = [{ name: "contact1" }, { name: "contact2", url: "url2", email: "email2@gmail.com" }]; + assert.deepEqual(parseContacts(contactsJson), result); + assert.deepEqual(errors, []); + }); + + describe("errors", () => { + it("should display an error and exit the process when the contacts is not valid json", () => { + const parseContacts = createContactsParser({ + logError, + exit + }); + + const unvalidJson = "]["; + + assert.throws(() => parseContacts(unvalidJson), { message: "process exited" }); + assert.deepEqual(errors, ["cli.errors.contacts.should_be_valid_json Unexpected token ']', \"][\" is not valid JSON"]); + }); + + it("should display an error and exit the process when the contacts is not an array", () => { + const parseContacts = createContactsParser({ + logError, + exit + }); + + const contactsJson = "{\"name\":\"contact1\"}"; + + assert.throws(() => parseContacts(contactsJson), { message: "process exited" }); + assert.deepEqual(errors, ["cli.errors.contacts.should_be_array"]); + }); + + it("should display an error when a contact is null", () => { + const parseContacts = createContactsParser({ + logError, + exit + }); + + const contactsJson = "[{\"name\": \"contact1\"},null,{\"name\":\"contact2\"," + + "\"url\":\"url2\",\"email\":\"email2@gmail.com\"},null]"; + assert.throws(() => parseContacts(contactsJson), { message: "process exited" }); + assert.deepEqual(errors, ["cli.errors.contacts.should_be_defined 1", "cli.errors.contacts.should_be_defined 3"]); + }); + }); +}); + From dc163111b8642ff5895512039921fc17ae42a1aa Mon Sep 17 00:00:00 2001 From: cgombauld Date: Mon, 9 Jun 2025 12:27:27 +0200 Subject: [PATCH 2/8] docs(contats) add doc for contacts arg --- docs/cli/auto.md | 17 +++++++++-------- docs/cli/cwd.md | 17 +++++++++-------- docs/cli/from.md | 13 +++++++------ 3 files changed, 25 insertions(+), 22 deletions(-) diff --git a/docs/cli/auto.md b/docs/cli/auto.md index bb85ff36..fc60e25e 100644 --- a/docs/cli/auto.md +++ b/docs/cli/auto.md @@ -20,11 +20,12 @@ $ nsecure auto --keep ## ⚙️ Available Options -| Name | Shortcut | Default Value | Description | -|---|---|---|--| -| `--depth` | `-d` | `Infinity` | Maximum tree depth to scan. | -| `--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)). | -| `--keep` | `-k` | `false` | Preserve JSON payload after execution. | -| `--developer` | `-d` | `false` | Launch the server in developer mode, enabling automatic HTML component refresh. | +| Name | Shortcut | Default Value | Description | +| ------------------------- | -------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--depth` | `-d` | `Infinity` | Maximum tree depth to scan. | +| `--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)). | +| `--keep` | `-k` | `false` | Preserve JSON payload after execution. | +| `--developer` | `-d` | `false` | Launch the server in developer mode, enabling automatic HTML component refresh. | +| `--contacts` | `-c` | `'[]'` | List of contacts to highlight. | diff --git a/docs/cli/cwd.md b/docs/cli/cwd.md index 64fe2c1a..07930494 100644 --- a/docs/cli/cwd.md +++ b/docs/cli/cwd.md @@ -10,11 +10,12 @@ $ nsecure cwd [options] ## ⚙️ Available Options -| Name | Shortcut | Default Value | Description | -|---|---|---|---| -| `--nolock` | `-n` | `false` | Do not use a lock file (package-lock.json or yarn.lock) for the analysis. | -| `--full` | `-f` | `false` | Perform a full analysis of the project, including all dependencies. | -| `--depth` | `-d` | `Infinity` | Maximum tree depth to scan. | -| `--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)). | +| Name | Shortcut | Default Value | Description | +| ------------------------- | -------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--nolock` | `-n` | `false` | Do not use a lock file (package-lock.json or yarn.lock) for the analysis. | +| `--full` | `-f` | `false` | Perform a full analysis of the project, including all dependencies. | +| `--depth` | `-d` | `Infinity` | Maximum tree depth to scan. | +| `--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. | diff --git a/docs/cli/from.md b/docs/cli/from.md index 999380c5..910402fe 100644 --- a/docs/cli/from.md +++ b/docs/cli/from.md @@ -18,9 +18,10 @@ $ nsecure from express@3.0.0 -o express-report ## ⚙️ Available Options -| Name | Shortcut | Default Value | Description | -|---|---|---|---| -| `--depth` | `-d` | `Infinity` | Maximum tree depth to scan. | -| `--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)). | +| Name | Shortcut | Default Value | Description | +| ------------------------- | -------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--depth` | `-d` | `Infinity` | Maximum tree depth to scan. | +| `--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. | From fe8eb0673c37d349153051045d9c23abb1957439 Mon Sep 17 00:00:00 2001 From: cgombauld Date: Mon, 9 Jun 2025 21:37:25 +0200 Subject: [PATCH 3/8] feat(contacts): parse strings instead of json --- bin/index.js | 2 +- i18n/english.js | 7 --- i18n/french.js | 7 --- src/commands/parsers/contacts.js | 46 ++++++-------- src/commands/scanner.js | 15 ++--- test/commands/parsers/contacts.test.js | 84 +++++++------------------- 6 files changed, 46 insertions(+), 115 deletions(-) diff --git a/bin/index.js b/bin/index.js index 552cc4fd..dc812e74 100755 --- a/bin/index.js +++ b/bin/index.js @@ -136,7 +136,7 @@ function defaultScannerCommand(name, options = {}) { const cmd = prog.command(name) .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("-c, --contacts", i18n.getTokenSync("cli.commands.option_contacts"), ""); if (includeOutput) { cmd.option("-o, --output", i18n.getTokenSync("cli.commands.option_output"), "nsecure-result"); diff --git a/i18n/english.js b/i18n/english.js index d242ea5f..9ac024a6 100644 --- a/i18n/english.js +++ b/i18n/english.js @@ -82,13 +82,6 @@ const cli = { invalidScannerVersion: tS`the payload has been scanned with version '${0}' and do not satisfies the required CLI range '${1}'`, regenerate: "please re-generate a new JSON payload using the CLI" }, - errors: { - contacts: { - should_be_valid_json: tS`Contacts: ${0}`, - should_be_array: "Contacts should be an array", - should_be_defined: tS`Contact at index ${0} should not be null` - } - } }; const ui = { diff --git a/i18n/french.js b/i18n/french.js index 15d099d0..63e66e78 100644 --- a/i18n/french.js +++ b/i18n/french.js @@ -82,13 +82,6 @@ const cli = { invalidScannerVersion: tS`le fichier d'analyse correspond à la version '${0}' du scanner et ne satisfait pas la range '${1}' attendu par la CLI`, regenerate: "veuillez re-générer un nouveau fichier d'analyse JSON en utilisant votre CLI" }, - errors: { - contacts: { - should_be_valid_json: tS`Contacts: ${0}`, - should_be_array: "Contacts doit etre un array", - should_be_defined: tS`Contact à index ${0} ne doit pas etre null` - } - } }; const ui = { diff --git a/src/commands/parsers/contacts.js b/src/commands/parsers/contacts.js index 9e3cbb3e..92a22a36 100644 --- a/src/commands/parsers/contacts.js +++ b/src/commands/parsers/contacts.js @@ -1,33 +1,23 @@ -export function createContactsParser({ logError, exit }) { - return (json) => { - const contacts = parseContacts({ json, logError, exit }); - if (!Array.isArray(contacts)) { - logError("cli.errors.contacts.should_be_array"); - exit(); - } - let hasError = false; - contacts.forEach((contact, i) => { - if (!contact) { - hasError = true; - logError("cli.errors.contacts.should_be_defined", i); - } - }); - if (hasError) { - exit(); - } - - return contacts; - }; +export function parseContacts(str) { + return str ? str.split(",").map(parseContact) : []; } -function parseContacts({ json, logError, exit }) { - try { - return JSON.parse(json); +function parseContact(str) { + const emailMatch = str.match(emailRegex()); + if (!emailMatch) { + return { name: str.trim() }; } - catch (err) { - logError("cli.errors.contacts.should_be_valid_json", err.message); - exit(); - - return null; + const email = emailMatch[0]; + const name = str.replace(email, "").trim(); + if (name) { + return { name, email }; } + + return { email }; +} + +const regex = "[^\\.\\s@:](?:[^\\s@:]*[^\\s@:\\.])?@[^\\.\\s@]+(?:\\.[^\\.\\s@]+)*"; + +function emailRegex() { + return new RegExp(regex, "g"); } diff --git a/src/commands/scanner.js b/src/commands/scanner.js index 5642cc91..01eb033a 100644 --- a/src/commands/scanner.js +++ b/src/commands/scanner.js @@ -14,13 +14,7 @@ import * as Scanner from "@nodesecure/scanner"; // Import Internal Dependencies import * as http from "./http.js"; import { appCache } from "../cache.js"; -import { createContactsParser } from "./parsers/contacts.js"; - -const parseContacts = createContactsParser({ - logError: (tokenName, param) => console.log(kleur.red().bold(param ? i18n.getTokenSync(tokenName, param) - : i18n.getTokenSync(tokenName))), - exit: () => process.exit() -}); +import { parseContacts } from "./parsers/contacts.js"; export async function auto(spec, options) { const { keep, ...commandOptions } = options; @@ -88,9 +82,10 @@ export async function from(spec, options) { const payload = await Scanner.from( spec, - { maxDepth, highlight: { - contacts: parseContacts(contacts) - } }, + { maxDepth, + highlight: { + contacts: parseContacts(contacts) + } }, initLogger(spec, !silent) ); diff --git a/test/commands/parsers/contacts.test.js b/test/commands/parsers/contacts.test.js index 18e49000..03c854f1 100644 --- a/test/commands/parsers/contacts.test.js +++ b/test/commands/parsers/contacts.test.js @@ -1,78 +1,38 @@ // Import Node.js Dependencies -import { it, describe, beforeEach } from "node:test"; +import { describe, it } from "node:test"; import assert from "node:assert/strict"; // Import Internal Dependencies -import { createContactsParser } from "../../../src/commands/parsers/contacts.js"; - -let errors = []; - -function exit() { - throw new Error("process exited"); -} - -beforeEach(() => { - errors = []; -}); - -function logError(token, param) { - if (param) { - errors.push(`${token} ${param}`); - } - else { - errors.push(token); - } -} +import { parseContacts } from "../../../src/commands/parsers/contacts.js"; describe("contacts parser", () => { - it("should successfully parse the contacts to highlight", () => { - const parseContacts = createContactsParser({ - logError, - exit - }); - - const contactsJson = "[{\"name\": \"contact1\"},{\"name\":\"contact2\",\"url\":\"url2\",\"email\":\"email2@gmail.com\"}]"; - const result = [{ name: "contact1" }, { name: "contact2", url: "url2", email: "email2@gmail.com" }]; - assert.deepEqual(parseContacts(contactsJson), result); - assert.deepEqual(errors, []); + it("should have no contacts", () => { + assert.deepEqual(parseContacts(""), []); }); - describe("errors", () => { - it("should display an error and exit the process when the contacts is not valid json", () => { - const parseContacts = createContactsParser({ - logError, - exit - }); - - const unvalidJson = "]["; - - assert.throws(() => parseContacts(unvalidJson), { message: "process exited" }); - assert.deepEqual(errors, ["cli.errors.contacts.should_be_valid_json Unexpected token ']', \"][\" is not valid JSON"]); - }); + it("should have a contact with a name", () => { + assert.deepEqual(parseContacts("sindre"), [{ name: "sindre" }]); + }); - it("should display an error and exit the process when the contacts is not an array", () => { - const parseContacts = createContactsParser({ - logError, - exit - }); + it("should trim names", () => { + assert.deepEqual(parseContacts(" matteo "), [{ name: "matteo" }]); + }); - const contactsJson = "{\"name\":\"contact1\"}"; + it("should have a contact with an email", () => { + assert.deepEqual(parseContacts("matteo@gmail.com"), [{ email: "matteo@gmail.com" }]); + }); - assert.throws(() => parseContacts(contactsJson), { message: "process exited" }); - assert.deepEqual(errors, ["cli.errors.contacts.should_be_array"]); - }); + it("should trim emails", () => { + assert.deepEqual(parseContacts(" sindre@gmail.com "), [{ email: "sindre@gmail.com" }]); + }); - it("should display an error when a contact is null", () => { - const parseContacts = createContactsParser({ - logError, - exit - }); + it("should parse names and emails", () => { + assert.deepEqual(parseContacts("sindre sindre@gmail.com"), [{ name: "sindre", email: "sindre@gmail.com" }]); + }); - const contactsJson = "[{\"name\": \"contact1\"},null,{\"name\":\"contact2\"," + - "\"url\":\"url2\",\"email\":\"email2@gmail.com\"},null]"; - assert.throws(() => parseContacts(contactsJson), { message: "process exited" }); - assert.deepEqual(errors, ["cli.errors.contacts.should_be_defined 1", "cli.errors.contacts.should_be_defined 3"]); - }); + it("should parse multiples contacts", () => { + assert.deepEqual(parseContacts("sindre sindre@gmail.com, matteo"), + [{ name: "sindre", email: "sindre@gmail.com" }, { name: "matteo" }]); }); }); From 04099d8664e770bc7987a7e85c9f40c3a5408970 Mon Sep 17 00:00:00 2001 From: cgombauld Date: Tue, 10 Jun 2025 09:24:46 +0200 Subject: [PATCH 4/8] feat(contacts): visually highlight contacts --- .../views/home/maintainers/maintainers.css | 4 ++++ .../views/home/maintainers/maintainers.js | 17 ++++++++++------- src/commands/parsers/contacts.js | 7 ++++--- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/public/components/views/home/maintainers/maintainers.css b/public/components/views/home/maintainers/maintainers.css index e3fc66a8..61adc784 100644 --- a/public/components/views/home/maintainers/maintainers.css +++ b/public/components/views/home/maintainers/maintainers.css @@ -26,6 +26,10 @@ body.dark .home--maintainers>.person { background: var(--dark-theme-primary-color); } +.home--maintainers> .highlighted{ + background: linear-gradient(to bottom, rgb(230, 240, 250) 0%, rgb(220, 235, 245) 100%); +} + .home--maintainers>.person:hover { border-color: var(--secondary-darker); cursor: pointer; diff --git a/public/components/views/home/maintainers/maintainers.js b/public/components/views/home/maintainers/maintainers.js index c80a35fe..54a5b2df 100644 --- a/public/components/views/home/maintainers/maintainers.js +++ b/public/components/views/home/maintainers/maintainers.js @@ -26,25 +26,25 @@ export class Maintainers { } render() { + const highlightedContacts = new Set(this.secureDataSet.data.highlighted.contacts + .map(({ name }) => name)); const authors = this.#highlightContacts([...this.secureDataSet.authors.entries()] - .sort((left, right) => right[1].packages.size - left[1].packages.size)); + .sort((left, right) => right[1].packages.size - left[1].packages.size), + highlightedContacts); document.getElementById("authors-count").innerHTML = authors.length; document.querySelector(".home--maintainers") - .appendChild(this.generate(authors)); + .appendChild(this.generate(authors, highlightedContacts)); } - #highlightContacts(authors) { - const highlightedContacts = new Set(this.secureDataSet.data.highlighted.contacts - .map(({ name }) => name)); - + #highlightContacts(authors, highlightedContacts) { const highlightedAuthors = authors.filter(([name]) => highlightedContacts.has(name)); const authorsRest = authors.filter(([name]) => !highlightedContacts.has(name)); return [...highlightedAuthors, ...authorsRest]; } - generate(authors) { + generate(authors, highlightedContacts) { const fragment = document.createDocumentFragment(); const hideItems = authors.length > this.maximumMaintainers; @@ -71,6 +71,9 @@ export class Maintainers { }) ] }); + if (highlightedContacts.has(name)) { + person.classList.add("highlighted"); + } if (hideItems && id >= this.maximumMaintainers) { person.classList.add("hidden"); } diff --git a/src/commands/parsers/contacts.js b/src/commands/parsers/contacts.js index 92a22a36..b8e55d56 100644 --- a/src/commands/parsers/contacts.js +++ b/src/commands/parsers/contacts.js @@ -1,3 +1,6 @@ +// CONSTANTS +const kEmailRegex = "[^\\.\\s@:](?:[^\\s@:]*[^\\s@:\\.])?@[^\\.\\s@]+(?:\\.[^\\.\\s@]+)*"; + export function parseContacts(str) { return str ? str.split(",").map(parseContact) : []; } @@ -16,8 +19,6 @@ function parseContact(str) { return { email }; } -const regex = "[^\\.\\s@:](?:[^\\s@:]*[^\\s@:\\.])?@[^\\.\\s@]+(?:\\.[^\\.\\s@]+)*"; - function emailRegex() { - return new RegExp(regex, "g"); + return new RegExp(kEmailRegex, "g"); } From c366130659494e14a2978cf1e5927ffbfff4a850 Mon Sep 17 00:00:00 2001 From: cgombauld Date: Fri, 13 Jun 2025 18:56:47 +0200 Subject: [PATCH 5/8] update deps --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c701ffb7..1175a45a 100644 --- a/package.json +++ b/package.json @@ -87,7 +87,7 @@ "@nodesecure/ossf-scorecard-sdk": "^3.2.1", "@nodesecure/rc": "^4.0.1", "@nodesecure/report": "^3.0.0", - "@nodesecure/scanner": "^6.4.0", + "@nodesecure/scanner": "^6.6.0", "@nodesecure/utils": "^2.2.0", "@nodesecure/vulnera": "^2.0.1", "@openally/result": "^1.3.0", From 471172d802244c5852470a08082650ba248614c6 Mon Sep 17 00:00:00 2001 From: cgombauld Date: Fri, 13 Jun 2025 21:05:56 +0200 Subject: [PATCH 6/8] feat(contacts) use arr instead of str --- bin/index.js | 2 +- docs/cli/auto.md | 2 +- docs/cli/cwd.md | 2 +- docs/cli/from.md | 2 +- src/commands/parsers/contacts.js | 4 ++-- src/commands/scanner.js | 6 ++++-- test/commands/parsers/contacts.test.js | 14 +++++++------- 7 files changed, 17 insertions(+), 15 deletions(-) diff --git a/bin/index.js b/bin/index.js index dc812e74..5af87307 100755 --- a/bin/index.js +++ b/bin/index.js @@ -136,7 +136,7 @@ function defaultScannerCommand(name, options = {}) { const cmd = prog.command(name) .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("-c, --contacts", i18n.getTokenSync("cli.commands.option_contacts"), []); if (includeOutput) { cmd.option("-o, --output", i18n.getTokenSync("cli.commands.option_output"), "nsecure-result"); diff --git a/docs/cli/auto.md b/docs/cli/auto.md index fc60e25e..6764da77 100644 --- a/docs/cli/auto.md +++ b/docs/cli/auto.md @@ -28,4 +28,4 @@ $ 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` | `-d` | `false` | Launch the server in developer mode, enabling automatic HTML component refresh. | -| `--contacts` | `-c` | `'[]'` | List of contacts to highlight. | +| `--contacts` | `-c` | `[]` | List of contacts to highlight. | diff --git a/docs/cli/cwd.md b/docs/cli/cwd.md index 07930494..32be1110 100644 --- a/docs/cli/cwd.md +++ b/docs/cli/cwd.md @@ -18,4 +18,4 @@ $ 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. | +| `--contacts` | `-c` | `[]` | List of contacts to highlight. | diff --git a/docs/cli/from.md b/docs/cli/from.md index 910402fe..d05d95f0 100644 --- a/docs/cli/from.md +++ b/docs/cli/from.md @@ -24,4 +24,4 @@ $ 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. | +| `--contacts` | `-c` | `[]` | List of contacts to highlight. | diff --git a/src/commands/parsers/contacts.js b/src/commands/parsers/contacts.js index b8e55d56..577082ec 100644 --- a/src/commands/parsers/contacts.js +++ b/src/commands/parsers/contacts.js @@ -1,8 +1,8 @@ // CONSTANTS const kEmailRegex = "[^\\.\\s@:](?:[^\\s@:]*[^\\s@:\\.])?@[^\\.\\s@]+(?:\\.[^\\.\\s@]+)*"; -export function parseContacts(str) { - return str ? str.split(",").map(parseContact) : []; +export function parseContacts(arr) { + return arr.map(parseContact); } function parseContact(str) { diff --git a/src/commands/scanner.js b/src/commands/scanner.js index 01eb033a..bd676d75 100644 --- a/src/commands/scanner.js +++ b/src/commands/scanner.js @@ -82,10 +82,12 @@ export async function from(spec, options) { const payload = await Scanner.from( spec, - { maxDepth, + { + maxDepth, highlight: { contacts: parseContacts(contacts) - } }, + } + }, initLogger(spec, !silent) ); diff --git a/test/commands/parsers/contacts.test.js b/test/commands/parsers/contacts.test.js index 03c854f1..631f73e2 100644 --- a/test/commands/parsers/contacts.test.js +++ b/test/commands/parsers/contacts.test.js @@ -7,31 +7,31 @@ import { parseContacts } from "../../../src/commands/parsers/contacts.js"; describe("contacts parser", () => { it("should have no contacts", () => { - assert.deepEqual(parseContacts(""), []); + assert.deepEqual(parseContacts([]), []); }); it("should have a contact with a name", () => { - assert.deepEqual(parseContacts("sindre"), [{ name: "sindre" }]); + assert.deepEqual(parseContacts(["sindre"]), [{ name: "sindre" }]); }); it("should trim names", () => { - assert.deepEqual(parseContacts(" matteo "), [{ name: "matteo" }]); + assert.deepEqual(parseContacts([" matteo "]), [{ name: "matteo" }]); }); it("should have a contact with an email", () => { - assert.deepEqual(parseContacts("matteo@gmail.com"), [{ email: "matteo@gmail.com" }]); + assert.deepEqual(parseContacts(["matteo@gmail.com"]), [{ email: "matteo@gmail.com" }]); }); it("should trim emails", () => { - assert.deepEqual(parseContacts(" sindre@gmail.com "), [{ email: "sindre@gmail.com" }]); + assert.deepEqual(parseContacts([" sindre@gmail.com "]), [{ email: "sindre@gmail.com" }]); }); it("should parse names and emails", () => { - assert.deepEqual(parseContacts("sindre sindre@gmail.com"), [{ name: "sindre", email: "sindre@gmail.com" }]); + assert.deepEqual(parseContacts(["sindre sindre@gmail.com"]), [{ name: "sindre", email: "sindre@gmail.com" }]); }); it("should parse multiples contacts", () => { - assert.deepEqual(parseContacts("sindre sindre@gmail.com, matteo"), + assert.deepEqual(parseContacts(["sindre sindre@gmail.com","matteo"]), [{ name: "sindre", email: "sindre@gmail.com" }, { name: "matteo" }]); }); }); From ec6c0f49ef3069435ec656ecf4de5591ebeb538b Mon Sep 17 00:00:00 2001 From: cgombauld Date: Fri, 13 Jun 2025 21:47:13 +0200 Subject: [PATCH 7/8] --amend --- src/commands/parsers/contacts.js | 4 ++-- src/commands/scanner.js | 4 ++-- test/commands/parsers/contacts.test.js | 12 ++++++------ 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/commands/parsers/contacts.js b/src/commands/parsers/contacts.js index 577082ec..9062acdd 100644 --- a/src/commands/parsers/contacts.js +++ b/src/commands/parsers/contacts.js @@ -1,8 +1,8 @@ // CONSTANTS const kEmailRegex = "[^\\.\\s@:](?:[^\\s@:]*[^\\s@:\\.])?@[^\\.\\s@]+(?:\\.[^\\.\\s@]+)*"; -export function parseContacts(arr) { - return arr.map(parseContact); +export function parseContacts(input) { + return Array.isArray(input) ? input.map(parseContact) : [parseContact(input)]; } function parseContact(str) { diff --git a/src/commands/scanner.js b/src/commands/scanner.js index bd676d75..0a80a501 100644 --- a/src/commands/scanner.js +++ b/src/commands/scanner.js @@ -82,11 +82,11 @@ export async function from(spec, options) { const payload = await Scanner.from( spec, - { + { maxDepth, highlight: { contacts: parseContacts(contacts) - } + } }, initLogger(spec, !silent) ); diff --git a/test/commands/parsers/contacts.test.js b/test/commands/parsers/contacts.test.js index 631f73e2..2ecf1ece 100644 --- a/test/commands/parsers/contacts.test.js +++ b/test/commands/parsers/contacts.test.js @@ -11,27 +11,27 @@ describe("contacts parser", () => { }); it("should have a contact with a name", () => { - assert.deepEqual(parseContacts(["sindre"]), [{ name: "sindre" }]); + assert.deepEqual(parseContacts("sindre"), [{ name: "sindre" }]); }); it("should trim names", () => { - assert.deepEqual(parseContacts([" matteo "]), [{ name: "matteo" }]); + assert.deepEqual(parseContacts(" matteo "), [{ name: "matteo" }]); }); it("should have a contact with an email", () => { - assert.deepEqual(parseContacts(["matteo@gmail.com"]), [{ email: "matteo@gmail.com" }]); + assert.deepEqual(parseContacts("matteo@gmail.com"), [{ email: "matteo@gmail.com" }]); }); it("should trim emails", () => { - assert.deepEqual(parseContacts([" sindre@gmail.com "]), [{ email: "sindre@gmail.com" }]); + assert.deepEqual(parseContacts(" sindre@gmail.com "), [{ email: "sindre@gmail.com" }]); }); it("should parse names and emails", () => { - assert.deepEqual(parseContacts(["sindre sindre@gmail.com"]), [{ name: "sindre", email: "sindre@gmail.com" }]); + assert.deepEqual(parseContacts("sindre sindre@gmail.com"), [{ name: "sindre", email: "sindre@gmail.com" }]); }); it("should parse multiples contacts", () => { - assert.deepEqual(parseContacts(["sindre sindre@gmail.com","matteo"]), + assert.deepEqual(parseContacts(["sindre sindre@gmail.com", "matteo"]), [{ name: "sindre", email: "sindre@gmail.com" }, { name: "matteo" }]); }); }); From d04b30b8027a1d1553df87131044bf0972003f72 Mon Sep 17 00:00:00 2001 From: cgombauld Date: Sat, 14 Jun 2025 16:20:00 +0200 Subject: [PATCH 8/8] feat(maintainers): can highlight by email --- .../views/home/maintainers/maintainers.css | 4 +++ .../views/home/maintainers/maintainers.js | 19 ++++++------ workspaces/vis-network/src/dataset.js | 20 +++++++++++- .../vis-network/test/dataset-payload.json | 31 +++++++++++++++++++ workspaces/vis-network/test/dataset.test.js | 16 +++++++++- 5 files changed, 78 insertions(+), 12 deletions(-) diff --git a/public/components/views/home/maintainers/maintainers.css b/public/components/views/home/maintainers/maintainers.css index 61adc784..23842f3f 100644 --- a/public/components/views/home/maintainers/maintainers.css +++ b/public/components/views/home/maintainers/maintainers.css @@ -30,6 +30,10 @@ body.dark .home--maintainers>.person { background: linear-gradient(to bottom, rgb(230, 240, 250) 0%, rgb(220, 235, 245) 100%); } +body.dark .home--maintainers > .highlighted { + background: linear-gradient(to right, rgb(11, 3, 31) 0%, rgba(46, 10, 10, 0.8) 100%); +} + .home--maintainers>.person:hover { border-color: var(--secondary-darker); cursor: pointer; diff --git a/public/components/views/home/maintainers/maintainers.js b/public/components/views/home/maintainers/maintainers.js index 54a5b2df..23ce1c4b 100644 --- a/public/components/views/home/maintainers/maintainers.js +++ b/public/components/views/home/maintainers/maintainers.js @@ -26,25 +26,24 @@ export class Maintainers { } render() { - const highlightedContacts = new Set(this.secureDataSet.data.highlighted.contacts - .map(({ name }) => name)); const authors = this.#highlightContacts([...this.secureDataSet.authors.entries()] - .sort((left, right) => right[1].packages.size - left[1].packages.size), - highlightedContacts); + .sort((left, right) => right[1].packages.size - left[1].packages.size)); document.getElementById("authors-count").innerHTML = authors.length; document.querySelector(".home--maintainers") - .appendChild(this.generate(authors, highlightedContacts)); + .appendChild(this.generate(authors)); } - #highlightContacts(authors, highlightedContacts) { - const highlightedAuthors = authors.filter(([name]) => highlightedContacts.has(name)); - const authorsRest = authors.filter(([name]) => !highlightedContacts.has(name)); + #highlightContacts(authors) { + const highlightedAuthors = authors + .filter(([_, contact]) => this.secureDataSet.isHighlighted(contact)); + + const authorsRest = authors.filter(([_, contact]) => !this.secureDataSet.isHighlighted(contact)); return [...highlightedAuthors, ...authorsRest]; } - generate(authors, highlightedContacts) { + generate(authors) { const fragment = document.createDocumentFragment(); const hideItems = authors.length > this.maximumMaintainers; @@ -71,7 +70,7 @@ export class Maintainers { }) ] }); - if (highlightedContacts.has(name)) { + if (this.secureDataSet.isHighlighted(data)) { person.classList.add("highlighted"); } if (hideItems && id >= this.maximumMaintainers) { diff --git a/workspaces/vis-network/src/dataset.js b/workspaces/vis-network/src/dataset.js index 5ae2f936..5a84bc11 100644 --- a/workspaces/vis-network/src/dataset.js +++ b/workspaces/vis-network/src/dataset.js @@ -13,6 +13,9 @@ export default class NodeSecureDataSet extends EventTarget { * @param {string[]} [options.warningsToIgnore=[]] * @param {"light"|"dark"} [options.theme] */ + + #highligthedContacts; + constructor(options = {}) { super(); const { @@ -20,7 +23,6 @@ export default class NodeSecureDataSet extends EventTarget { warningsToIgnore = [], theme = "light" } = options; - this.flagsToIgnore = new Set(flagsToIgnore); this.warningsToIgnore = new Set(warningsToIgnore); this.theme = theme; @@ -76,6 +78,18 @@ export default class NodeSecureDataSet extends EventTarget { this.warnings = data.warnings; + this.#highligthedContacts = data.highlighted.contacts + .reduce((acc, { name, email }) => { + if (name) { + acc.names.add(name); + } + if (email) { + acc.emails.add(email); + } + + return acc; + }, { names: new Set(), emails: new Set() }); + const dataEntries = Object.entries(data.dependencies); this.dependenciesCount = dataEntries.length; @@ -210,4 +224,8 @@ export default class NodeSecureDataSet extends EventTarget { return { nodes, edges }; } + + isHighlighted(contact) { + return this.#highligthedContacts.names.has(contact.name) || this.#highligthedContacts.emails.has(contact.email); + } } diff --git a/workspaces/vis-network/test/dataset-payload.json b/workspaces/vis-network/test/dataset-payload.json index 96c81a12..af095da8 100644 --- a/workspaces/vis-network/test/dataset-payload.json +++ b/workspaces/vis-network/test/dataset-payload.json @@ -1,6 +1,37 @@ { "id": "abcde", "rootDepencyName": "pkg1", + "highlighted": { + "contacts": [ + { + "email": "gentilhomme.thomas@gmail.com", + "dependencies": [ + "frequency-set", + "sec-literal", + "js-x-ray" + ] + }, + { + "name": "Rich Harris", + "email": "rich.harris@gmail.com", + "dependencies": [ + "frequency-set", + "sec-literal", + "js-x-ray" + ] + }, + { + "name": "Sindre Sorhus", + "dependencies": [ + "is-svg", + "ansi-regex", + "strip-ansi", + "is-fullwidth-code-point", + "string-width" + ] + } + ] + }, "dependencies": { "pkg2": { "versions": { diff --git a/workspaces/vis-network/test/dataset.test.js b/workspaces/vis-network/test/dataset.test.js index 8782be6b..7eaa6590 100644 --- a/workspaces/vis-network/test/dataset.test.js +++ b/workspaces/vis-network/test/dataset.test.js @@ -11,7 +11,6 @@ const dataSetPayload = await getDataSetPayload(); test("NodeSecureDataSet.init with given payload", async() => { const nsDataSet = new NodeSecureDataSet(); await nsDataSet.init(dataSetPayload); - assert.equal(nsDataSet.data, dataSetPayload, "should set data"); }); @@ -41,6 +40,21 @@ test("NodeSecureDataSet.computeExtensions", () => { assert.equal(nsDataSet.extensions[".js"], 2, "should have 2 '.js' extensions'"); }); +test("NodeSecureDataSet.isHighlighted", async() => { + const nsDataSet = new NodeSecureDataSet(); + await nsDataSet.init(dataSetPayload); + assert.equal(nsDataSet.isHighlighted({ name: "Unknown" }), false, "should not be hightlighted"); + assert.equal(nsDataSet.isHighlighted({ name: "Sindre Sorhus" }), true, "name: Sindre Sorhus should be hightlighted"); + assert.equal(nsDataSet.isHighlighted({ name: "Rich Harris" }), true, "name: Rich Harris should be hightlighted"); + assert.equal(nsDataSet.isHighlighted({ email: "rich.harris@gmail.com" }), + true, + "email: rich.harris@gmail.com should be hightlighted"); + + assert.equal(nsDataSet.isHighlighted({ email: "gentilhomme.thomas@gmail.com" }), + true, + "email: gentilhomme.thomas@gmail.com should be hightlighted"); +}); + test("NodeSecureDataSet.computeLicenses", () => { const nsDataSet = new NodeSecureDataSet(); nsDataSet.computeLicense("MIT");