diff --git a/bin/index.js b/bin/index.js index 99911d5c..5af87307 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/docs/cli/auto.md b/docs/cli/auto.md index bb85ff36..6764da77 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..32be1110 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..d05d95f0 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. | diff --git a/i18n/english.js b/i18n/english.js index f2819ada..9ac024a6 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,7 +81,7 @@ 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" - } + }, }; const ui = { diff --git a/i18n/french.js b/i18n/french.js index b41348fa..63e66e78 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,7 +81,7 @@ 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" - } + }, }; const ui = { 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", diff --git a/public/components/views/home/maintainers/maintainers.css b/public/components/views/home/maintainers/maintainers.css index e3fc66a8..23842f3f 100644 --- a/public/components/views/home/maintainers/maintainers.css +++ b/public/components/views/home/maintainers/maintainers.css @@ -26,6 +26,14 @@ 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%); +} + +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 e2379897..23ce1c4b 100644 --- a/public/components/views/home/maintainers/maintainers.js +++ b/public/components/views/home/maintainers/maintainers.js @@ -26,14 +26,23 @@ 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 highlightedAuthors = authors + .filter(([_, contact]) => this.secureDataSet.isHighlighted(contact)); + + const authorsRest = authors.filter(([_, contact]) => !this.secureDataSet.isHighlighted(contact)); + + return [...highlightedAuthors, ...authorsRest]; + } + generate(authors) { const fragment = document.createDocumentFragment(); const hideItems = authors.length > this.maximumMaintainers; @@ -61,6 +70,9 @@ export class Maintainers { }) ] }); + if (this.secureDataSet.isHighlighted(data)) { + 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 new file mode 100644 index 00000000..9062acdd --- /dev/null +++ b/src/commands/parsers/contacts.js @@ -0,0 +1,24 @@ +// CONSTANTS +const kEmailRegex = "[^\\.\\s@:](?:[^\\s@:]*[^\\s@:\\.])?@[^\\.\\s@]+(?:\\.[^\\.\\s@]+)*"; + +export function parseContacts(input) { + return Array.isArray(input) ? input.map(parseContact) : [parseContact(input)]; +} + +function parseContact(str) { + const emailMatch = str.match(emailRegex()); + if (!emailMatch) { + return { name: str.trim() }; + } + const email = emailMatch[0]; + const name = str.replace(email, "").trim(); + if (name) { + return { name, email }; + } + + return { email }; +} + +function emailRegex() { + return new RegExp(kEmailRegex, "g"); +} diff --git a/src/commands/scanner.js b/src/commands/scanner.js index 89d0ab40..0a80a501 100644 --- a/src/commands/scanner.js +++ b/src/commands/scanner.js @@ -14,14 +14,22 @@ import * as Scanner from "@nodesecure/scanner"; // Import Internal Dependencies import * as http from "./http.js"; import { appCache } from "../cache.js"; +import { parseContacts } from "./parsers/contacts.js"; 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 +63,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 +78,16 @@ 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..2ecf1ece --- /dev/null +++ b/test/commands/parsers/contacts.test.js @@ -0,0 +1,38 @@ +// Import Node.js Dependencies +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; + +// Import Internal Dependencies +import { parseContacts } from "../../../src/commands/parsers/contacts.js"; + +describe("contacts parser", () => { + it("should have no contacts", () => { + assert.deepEqual(parseContacts([]), []); + }); + + it("should have a contact with a name", () => { + assert.deepEqual(parseContacts("sindre"), [{ name: "sindre" }]); + }); + + it("should trim names", () => { + assert.deepEqual(parseContacts(" matteo "), [{ name: "matteo" }]); + }); + + it("should have a contact with an email", () => { + assert.deepEqual(parseContacts("matteo@gmail.com"), [{ email: "matteo@gmail.com" }]); + }); + + it("should trim emails", () => { + 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" }]); + }); + + it("should parse multiples contacts", () => { + assert.deepEqual(parseContacts(["sindre sindre@gmail.com", "matteo"]), + [{ name: "sindre", email: "sindre@gmail.com" }, { name: "matteo" }]); + }); +}); + 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");