Skip to content

feat(contacts): highlight contacts #504

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jun 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
Expand Down
17 changes: 9 additions & 8 deletions docs/cli/auto.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
17 changes: 9 additions & 8 deletions docs/cli/cwd.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
13 changes: 7 additions & 6 deletions docs/cli/from.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. |
3 changes: 2 additions & 1 deletion i18n/english.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 = {
Expand Down
3 changes: 2 additions & 1 deletion i18n/french.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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 = {
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
8 changes: 8 additions & 0 deletions public/components/views/home/maintainers/maintainers.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
16 changes: 14 additions & 2 deletions public/components/views/home/maintainers/maintainers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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");
}
Expand Down
24 changes: 24 additions & 0 deletions src/commands/parsers/contacts.js
Original file line number Diff line number Diff line change
@@ -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");
}
27 changes: 21 additions & 6 deletions src/commands/scanner.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,22 @@
// 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)
}
};

Check warning on line 28 in src/commands/scanner.js

View check run for this annotation

Codecov / codecov/patch

src/commands/scanner.js#L22-L28

Added lines #L22 - L28 were not covered by tests
const payloadFile = await (
typeof spec === "string" ?
from(spec, commandOptions) :
cwd(commandOptions)
from(spec, optionsWithContacts) :
cwd(optionsWithContacts)

Check warning on line 32 in src/commands/scanner.js

View check run for this annotation

Codecov / codecov/patch

src/commands/scanner.js#L31-L32

Added lines #L31 - L32 were not covered by tests
);
try {
if (payloadFile !== null) {
Expand Down Expand Up @@ -55,24 +63,31 @@
nolock,
full,
vulnerabilityStrategy,
silent
silent,
contacts

Check warning on line 67 in src/commands/scanner.js

View check run for this annotation

Codecov / codecov/patch

src/commands/scanner.js#L66-L67

Added lines #L66 - L67 were not covered by tests
} = options;

const payload = await Scanner.cwd(
process.cwd(),
{ maxDepth, usePackageLock: !nolock, fullLockMode: full, vulnerabilityStrategy },
{ maxDepth, usePackageLock: !nolock, fullLockMode: full, vulnerabilityStrategy, highlight:
{ contacts: parseContacts(contacts) } },

Check warning on line 73 in src/commands/scanner.js

View check run for this annotation

Codecov / codecov/patch

src/commands/scanner.js#L72-L73

Added lines #L72 - L73 were not covered by tests
initLogger(void 0, !silent)
);

return await logAndWrite(payload, output, { local: true });
}

export async function from(spec, options) {
const { depth: maxDepth = Infinity, output, silent } = options;
const { depth: maxDepth = Infinity, output, silent, contacts } = options;

Check warning on line 81 in src/commands/scanner.js

View check run for this annotation

Codecov / codecov/patch

src/commands/scanner.js#L81

Added line #L81 was not covered by tests

const payload = await Scanner.from(
spec,
{ maxDepth },
{
maxDepth,
highlight: {
contacts: parseContacts(contacts)
}
},

Check warning on line 90 in src/commands/scanner.js

View check run for this annotation

Codecov / codecov/patch

src/commands/scanner.js#L85-L90

Added lines #L85 - L90 were not covered by tests
initLogger(spec, !silent)
);

Expand Down
38 changes: 38 additions & 0 deletions test/commands/parsers/contacts.test.js
Original file line number Diff line number Diff line change
@@ -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" }]);
});
});

20 changes: 19 additions & 1 deletion workspaces/vis-network/src/dataset.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ export default class NodeSecureDataSet extends EventTarget {
* @param {string[]} [options.warningsToIgnore=[]]
* @param {"light"|"dark"} [options.theme]
*/

#highligthedContacts;

constructor(options = {}) {
super();
const {
flagsToIgnore = [],
warningsToIgnore = [],
theme = "light"
} = options;

this.flagsToIgnore = new Set(flagsToIgnore);
this.warningsToIgnore = new Set(warningsToIgnore);
this.theme = theme;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);
}
}
31 changes: 31 additions & 0 deletions workspaces/vis-network/test/dataset-payload.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
Loading