diff --git a/README.md b/README.md index 0671430e..f0e81faf 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ For complete details on each command, refer to the following documents: - [`config`](./docs/cli/config.md) - [`cache`](./docs/cli/cache.md) - [`extract integrity`](./docs/cli/extract-integrity.md) +- [`stats`](./docs/cli/stats.md) Each link provides access to the full documentation for the command, including additional details, options, and usage examples. diff --git a/bin/index.js b/bin/index.js index 6aa098d2..b8c341d7 100755 --- a/bin/index.js +++ b/bin/index.js @@ -135,6 +135,12 @@ prog .example("nsecure extract integrity lodash@^4.1.2") .action(commands.extractIntegrity.main); +prog + .command("stats") + .describe(i18n.getTokenSync("cli.commands.stats.desc")) + .example("nsecure stats") + .action(commands.stats.main); + prog.parse(process.argv); function defaultScannerCommand(name, options = {}) { diff --git a/docs/cli/images/stats.PNG b/docs/cli/images/stats.PNG new file mode 100644 index 00000000..baae072f Binary files /dev/null and b/docs/cli/images/stats.PNG differ diff --git a/docs/cli/stats.md b/docs/cli/stats.md new file mode 100644 index 00000000..27bf143e --- /dev/null +++ b/docs/cli/stats.md @@ -0,0 +1,19 @@ +## 📝 Command `stats` + +The `stats` displays the statistics of the last performed scan such as : + +- The total execution time of the scan +- The total number of API calls made +- every stats about the API calls made +- The total number of errors encountered +- every error encountered + +

+ +

+ +## 📜 Syntax + +```bash +$ nsecure stats +``` diff --git a/i18n/arabic.js b/i18n/arabic.js index 18ff9c0d..25227d35 100644 --- a/i18n/arabic.js +++ b/i18n/arabic.js @@ -8,7 +8,7 @@ const cli = { successfully_written_json: tS`تم كتابة ملف النتائج بنجاح في: ${0}`, http_server_started: "تم تشغيل خادم HTTP على:", missingEnv: tS`متغير البيئة ${0} مفقود!`, - stat: tS`${0} ${1} في ${2}`, + stat: tS`${0}${1} في ${2}`, error: { name: tS`اسم ${0}: ${1}`, message: tS`الرسالة: ${0}`, @@ -88,6 +88,13 @@ const cli = { missingSpecVersion: tS`يجب تحديد إصدار لحزمة '${0}'.`, invalidSpec: tS`مواصفات الحزمة '${0}' غير صالحة.`, specNotFound: tS`لم يتم العثور على مواصفات الحزمة '${0}' في سجل npm.` + }, + stats: { + desc: "عرض إحصائيات المسح.", + elapsed: tS`مدة المسح: ${0}`, + stats: tS`عدد استدعاءات API: ${0}`, + error: "يجب إجراء مسح قبل عرض الإحصائيات.", + errors: tS`عدد الأخطاء: ${0}` } }, startHttp: { diff --git a/i18n/english.js b/i18n/english.js index 85a4f833..525ab570 100644 --- a/i18n/english.js +++ b/i18n/english.js @@ -10,7 +10,7 @@ const cli = { successfully_written_json: tS`Successfully written results file at: ${0}`, http_server_started: "HTTP Server started on:", missingEnv: tS`Environment variable ${0} is missing!`, - stat: tS`${0} ${1} in ${2}`, + stat: tS`${0}${1} in ${2}`, error: { name: tS`${0} name: ${1}`, message: tS`Message: ${0}`, @@ -90,6 +90,13 @@ const cli = { missingSpecVersion: tS`You must specify a version for '${0}' package.`, invalidSpec: tS`The package spec '${0}' is invalid.`, specNotFound: tS`The package spec '${0}' could not be found from the npm registry.` + }, + stats: { + desc: "Display the stats of a scan.", + elapsed: tS`Scan duration: ${0}`, + stats: tS`API calls count: ${0}`, + error: "A scan must be performed before displaying stats.", + errors: tS`Error count: ${0}` } }, startHttp: { diff --git a/i18n/french.js b/i18n/french.js index 419ca2f3..c20c78d6 100644 --- a/i18n/french.js +++ b/i18n/french.js @@ -10,7 +10,7 @@ const cli = { successfully_written_json: tS`Ecriture du fichier de résultats réalisée avec succès ici : ${0}`, http_server_started: "Serveur HTTP démarré sur :", missingEnv: tS`La variable d'environnement ${0} est manquante!`, - stat: tS`${0} ${1} en ${2}`, + stat: tS`${0}${1} en ${2}`, error: { name: tS`Nom ${0}: ${1}`, message: tS`Message: ${0}`, @@ -90,6 +90,13 @@ const cli = { missingSpecVersion: tS`Vous devez spécifier une version pour le package '${0}'.`, invalidSpec: tS`La spécification '${0}' est invalide.`, specNotFound: tS`La spécification '${0}' n'a pas pu être trouvée dans le registre npm.` + }, + stats: { + desc: "Afficher les statistiques d'un scan.", + elapsed: tS`Durée du scan: ${0}`, + stats: tS`Nombre d'appels API: ${0}`, + error: "Un scan doit être effectué avant d'afficher les statistiques.", + errors: tS`Nombre d'erreurs: ${0}` } }, startHttp: { diff --git a/i18n/turkish.js b/i18n/turkish.js index e1c26b80..636db10c 100644 --- a/i18n/turkish.js +++ b/i18n/turkish.js @@ -10,7 +10,7 @@ const cli = { successfully_written_json: tS`Sonuç dosyası başarıyla yazıldı: ${0}`, http_server_started: "HTTP Sunucusu başlatıldı:", missingEnv: tS`${0} ortam değişkeni eksik!`, - stat: tS`${0} ${1} içinde ${2}`, + stat: tS`${0}${1} içinde ${2}`, error: { name: tS`${0} adı: ${1}`, message: tS`Mesaj: ${0}`, @@ -90,6 +90,13 @@ const cli = { missingSpecVersion: tS`'${0}' paketi için bir sürüm belirtmelisiniz.`, invalidSpec: tS`'${0}' paket özelliği geçersiz.`, specNotFound: tS`'${0}' paket özelliği npm kayıt defterinde bulunamadı.` + }, + stats: { + desc: "Bir taramanın istatistiklerini görüntüle.", + elapsed: tS`Tarama süresi: ${0}`, + stats: tS`API çağrı sayısı: ${0}`, + error: "İstatistikleri görüntülemeden önce bir tarama yapılmalıdır.", + errors: tS`Hata sayısı: ${0}` } }, startHttp: { diff --git a/src/commands/index.js b/src/commands/index.js index 7d08a433..f0ae1341 100644 --- a/src/commands/index.js +++ b/src/commands/index.js @@ -8,3 +8,4 @@ export * as scorecard from "./scorecard.js"; export * as report from "./report.js"; export * as cache from "./cache.js"; export * as extractIntegrity from "./extract-integrity.js"; +export * as stats from "./stats.js"; diff --git a/src/commands/loggers/logger.js b/src/commands/loggers/logger.js new file mode 100644 index 00000000..27fd2a3f --- /dev/null +++ b/src/commands/loggers/logger.js @@ -0,0 +1,78 @@ +// Import Third-party Dependencies +import * as i18n from "@nodesecure/i18n"; +import ms from "ms"; + +// Import Internal Dependencies +import kleur from "../../utils/styleText.js"; + +export function log(token, ...args) { + console.log(kleur.white().bold(i18n.getTokenSync(token, ...args))); +} + +export function logError(token, ...args) { + console.log(kleur.red().bold(i18n.getTokenSync(token, ...args))); +} + +export function logScannerStat(stat, isVerbose = true) { + console.log(kleur.bold.white( + i18n.getTokenSync("cli.stat", + isVerbose ? kleur.blue().bold("verbose ") : "", + stat.name, + colorExecutionTime(stat.executionTime) + ))); +} + +export function logScannerError(error, phase) { + console.log(kleur.bold.white( + i18n.getTokenSync("cli.error.name", + kleur.red().bold("error"), + error.name + ))); + + if (error.message) { + console.log(i18n.getTokenSync("cli.error.message", + error.message + )); + } + + if (phase) { + console.log(i18n.getTokenSync("cli.error.phase", + phase + )); + } + + if (error.statusCode) { + console.log(i18n.getTokenSync("cli.error.statusCode", + error.statusCode + )); + } + + console.log(i18n.getTokenSync("cli.error.executionTime", + colorExecutionTime(error.executionTime) + )); + + if (error.stack) { + console.log(i18n.getTokenSync("cli.error.stack", + error.stack + )); + } +} + +export function formatMs(time) { + return ms(Number(time.toFixed(2))); +} + +function colorExecutionTime(timeMs) { + const formatted = formatMs(timeMs); + if (timeMs <= 1_000) { + return kleur.green().bold(formatted); + } + else if (timeMs <= 5_000) { + return kleur.cyan().bold(formatted); + } + else if (timeMs <= 30_000) { + return kleur.yellow().bold(formatted); + } + + return kleur.red().bold(formatted); +} diff --git a/src/commands/scanner.js b/src/commands/scanner.js index ba48a2b5..adbf20d1 100644 --- a/src/commands/scanner.js +++ b/src/commands/scanner.js @@ -7,13 +7,13 @@ import events from "node:events"; import semver from "semver"; import filenamify from "filenamify"; import { Spinner } from "@topcli/spinner"; -import ms from "ms"; import * as i18n from "@nodesecure/i18n"; import * as scanner from "@nodesecure/scanner"; // Import Internal Dependencies import kleur from "../utils/styleText.js"; import * as http from "./http.js"; +import { logScannerStat, logScannerError, formatMs } from "./loggers/logger.js"; import { parseContacts } from "./parsers/contacts.js"; export async function auto(spec, options) { @@ -192,77 +192,19 @@ function initLogger(spec, verbose = true) { logger.on("stat", (stat) => { stopSpinners(); - console.log(kleur.bold.white( - i18n.getTokenSync("cli.stat", - kleur.blue().bold("verbose"), - stat.name, - colorExecutionTime(stat.executionTime) - ))); + logScannerStat(stat); startSpinners(); }); logger.on("error", (error, phase) => { stopSpinners(); - - console.log(kleur.bold.white( - i18n.getTokenSync("cli.error.name", - kleur.red().bold("error"), - error.name - ))); - - if (error.message) { - console.log(i18n.getTokenSync("cli.error.message", - error.message - )); - } - - if (phase) { - console.log(i18n.getTokenSync("cli.error.phase", - phase - )); - } - - if (error.statusCode) { - console.log(i18n.getTokenSync("cli.error.statusCode", - error.statusCode - )); - } - - console.log(i18n.getTokenSync("cli.error.executionTime", - kleur.cyan().bold(formatMs(error.executionTime)) - )); - - if (error.stack) { - console.log(i18n.getTokenSync("cli.error.stack", - error.stack - )); - } - + logScannerError(error, phase); startSpinners(); }); return logger; } -function formatMs(time) { - return ms(Number(time.toFixed(2))); -} - -function colorExecutionTime(timeMs) { - const formatted = formatMs(timeMs); - if (timeMs <= 1_000) { - return kleur.green().bold(formatted); - } - else if (timeMs <= 5_000) { - return kleur.cyan().bold(formatted); - } - else if (timeMs <= 30_000) { - return kleur.yellow().bold(formatted); - } - - return kleur.red().bold(formatted); -} - function stopSpinners() { spinners.forEach((spinner) => { if (!spinner.succeeded) { diff --git a/src/commands/stats.js b/src/commands/stats.js new file mode 100644 index 00000000..a8cd5051 --- /dev/null +++ b/src/commands/stats.js @@ -0,0 +1,42 @@ +// Import Node.js Dependencies +import { readFile } from "node:fs/promises"; +import path from "node:path"; + +// Import Internal Dependencies +import { logScannerStat, logScannerError, log, logError, formatMs } from "./loggers/logger.js"; + +export async function main(options) { + const { getScanResult = getScanFromFile, logger = { + logScannerStat, + logScannerError, + log, + logError + } } = options; + try { + const scanResult = await getScanResult(); + const { metadata } = scanResult; + + logger.log("cli.commands.stats.elapsed", formatMs(metadata.executionTime)); + logger.log("cli.commands.stats.stats", metadata.apiCallsCount); + metadata.apiCalls.forEach((call) => { + logger.logScannerStat(call, false); + }); + if (metadata.errorCount === 0) { + return; + } + logger.log("cli.commands.stats.errors", metadata.errorCount); + metadata.errors.forEach((error) => { + logger.logScannerError(error); + }); + } + catch { + logger.logError("cli.commands.stats.error"); + } +} + +async function getScanFromFile() { + const projectRootDir = path.join(import.meta.dirname, "..", ".."); + const filePath = path.join(projectRootDir, "nsecure-result.json"); + + return JSON.parse(await readFile(filePath, "utf8")); +} diff --git a/test/commands/stats.test.js b/test/commands/stats.test.js new file mode 100644 index 00000000..a6f7ad27 --- /dev/null +++ b/test/commands/stats.test.js @@ -0,0 +1,119 @@ +// Import Node.js Dependencies +import assert from "node:assert"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { describe, test } from "node:test"; + +// Import Internal Dependencies +import { main } from "../../src/commands/stats.js"; + +describe("stats", () => { + test("it should log stats and errors from the scan result", async(t) => { + const scanResult = JSON.parse(await readFile(path.join(import.meta.dirname, "..", "fixtures", "result-test3.json"), "utf8")); + + async function getScanResult() { + return Promise.resolve(scanResult); + } + + const logger = { + logScannerStat: t.mock.fn(), + logScannerError: t.mock.fn(), + log: t.mock.fn(), + logError: t.mock.fn() + }; + + await main({ + getScanResult, + logger + }); + + assert.deepEqual(logger.log.mock.calls[0].arguments, ["cli.commands.stats.elapsed", "771ms"]); + assert.deepEqual(logger.log.mock.calls[1].arguments, ["cli.commands.stats.stats", 3]); + assert.deepEqual(logger.logScannerStat.mock.calls[0].arguments, [{ + name: "pacote.manifest react@19.2.4", + startedAt: 1774601089504, + executionTime: 20 + }, false]); + assert.deepEqual(logger.logScannerStat.mock.calls[1].arguments, [{ + name: "pacote.extract react@19.2.4", + startedAt: 1774601089529, + executionTime: 83 + }, false]); + assert.deepEqual(logger.logScannerStat.mock.calls[2].arguments, [{ + name: "tarball.scanDirOrArchive react@19.2.4", + startedAt: 1774601089612, + executionTime: 247 + }, false]); + assert.deepEqual(logger.log.mock.calls[2].arguments, ["cli.commands.stats.errors", 2]); + assert.deepEqual(logger.logScannerError.mock.calls[0].arguments, [{ + name: "pacote.extract react@19.2.4" + }]); + assert.deepEqual(logger.logScannerError.mock.calls[1].arguments, [{ + name: "tarball.scanDirOrArchive react@19.2.4", + message: "something went wrong !" + }]); + }); + + test("should display an error message when no scan has been done", async(t) => { + async function getScanResult() { + throw new Error("file does not exist"); + } + + const logger = { + logScannerStat: t.mock.fn(), + logScannerError: t.mock.fn(), + log: t.mock.fn(), + logError: t.mock.fn() + }; + + await main({ + getScanResult, + logger + }); + + assert.deepEqual(logger.logError.mock.calls[0].arguments, ["cli.commands.stats.error"]); + assert.strictEqual(logger.log.mock.callCount(), 0); + assert.strictEqual(logger.logScannerStat.mock.callCount(), 0); + assert.strictEqual(logger.logScannerError.mock.callCount(), 0); + }); + + test("should not log the error part when there is none", async(t) => { + const scanResult = JSON.parse(await readFile(path.join(import.meta.dirname, "..", "fixtures", "result-test4.json"), "utf8")); + + async function getScanResult() { + return Promise.resolve(scanResult); + } + + const logger = { + logScannerStat: t.mock.fn(), + logScannerError: t.mock.fn(), + log: t.mock.fn(), + logError: t.mock.fn() + }; + + await main({ + getScanResult, + logger + }); + + assert.deepEqual(logger.log.mock.calls[0].arguments, ["cli.commands.stats.elapsed", "771ms"]); + assert.deepEqual(logger.log.mock.calls[1].arguments, ["cli.commands.stats.stats", 3]); + assert.deepEqual(logger.logScannerStat.mock.calls[0].arguments, [{ + name: "pacote.manifest react@19.2.4", + startedAt: 1774601089504, + executionTime: 20 + }, false]); + assert.deepEqual(logger.logScannerStat.mock.calls[1].arguments, [{ + name: "pacote.extract react@19.2.4", + startedAt: 1774601089529, + executionTime: 83 + }, false]); + assert.deepEqual(logger.logScannerStat.mock.calls[2].arguments, [{ + name: "tarball.scanDirOrArchive react@19.2.4", + startedAt: 1774601089612, + executionTime: 247 + }, false]); + assert.equal(logger.log.mock.calls[2], undefined); + }); +}); + diff --git a/test/fixtures/result-test3.json b/test/fixtures/result-test3.json new file mode 100644 index 00000000..8dff31a6 --- /dev/null +++ b/test/fixtures/result-test3.json @@ -0,0 +1,1028 @@ +{ + "id": "CBuXRl", + "rootDependency": { + "name": "react", + "version": "19.2.4", + "integrity": "sha512-Lfi0RfVh0gdrtD6yI6AInys+HY8Ki8E/SlIpUYm3gXzlKOXW7zvRMMC6sMQ3+3ww+ZSF5cTnOafW2T5HjtwQmg==" + }, + "scannerVersion": "10.7.0", + "vulnerabilityStrategy": "github-advisory", + "warnings": [], + "highlighted": { + "contacts": [], + "packages": [], + "identifiers": [] + }, + "dependencies": { + "react": { + "versions": { + "19.2.4": { + "id": 0, + "type": "cjs", + "usedBy": {}, + "isDevDependency": false, + "existOnRemoteRegistry": true, + "flags": [ + "hasMissingOrUnusedDependency", + "hasWarnings" + ], + "warnings": [ + { + "kind": "log-usage", + "location": [ + [ + [ + 310, + 10 + ], + [ + 310, + 30 + ] + ] + ], + "source": "JS-X-Ray", + "i18n": "sast_warnings.log_usage", + "severity": "Information", + "experimental": false, + "value": "console.error", + "file": "cjs/react.production.js" + }, + { + "kind": "insecure-random", + "location": [ + [ + 529, + 43 + ], + [ + 529, + 56 + ] + ], + "source": "JS-X-Ray", + "i18n": "sast_warnings.insecure_random", + "severity": "Information", + "experimental": false, + "value": null, + "file": "cjs/react.development.js" + }, + { + "kind": "log-usage", + "location": [ + [ + [ + 17, + 10 + ], + [ + 21, + 11 + ] + ], + [ + [ + 413, + 14 + ], + [ + 415, + 15 + ] + ], + [ + [ + 1014, + 10 + ], + [ + 1016, + 11 + ] + ], + [ + [ + 1176, + 12 + ], + [ + 1178, + 13 + ] + ], + [ + [ + 1222, + 8 + ], + [ + 1224, + 9 + ] + ], + [ + [ + 1238, + 8 + ], + [ + 1240, + 9 + ] + ], + [ + [ + 1245, + 8 + ], + [ + 1247, + 9 + ] + ], + [ + [ + 40, + 9 + ], + [ + 44, + 9 + ] + ], + [ + [ + 112, + 12 + ], + [ + 114, + 13 + ] + ], + [ + [ + 179, + 10 + ], + [ + 182, + 11 + ] + ], + [ + [ + 194, + 8 + ], + [ + 196, + 9 + ] + ], + [ + [ + 502, + 12 + ], + [ + 505, + 13 + ] + ], + [ + [ + 507, + 12 + ], + [ + 510, + 13 + ] + ], + [ + [ + 518, + 8 + ], + [ + 520, + 9 + ] + ], + [ + [ + 539, + 16 + ], + [ + 541, + 17 + ] + ], + [ + [ + 556, + 8 + ], + [ + 558, + 9 + ] + ], + [ + [ + 741, + 14 + ], + [ + 741, + 34 + ] + ], + [ + [ + 835, + 12 + ], + [ + 837, + 13 + ] + ], + [ + [ + 890, + 14 + ], + [ + 892, + 15 + ] + ], + [ + [ + 1064, + 10 + ], + [ + 1066, + 11 + ] + ], + [ + [ + 1068, + 12 + ], + [ + 1071, + 13 + ] + ], + [ + [ + 1074, + 12 + ], + [ + 1079, + 13 + ] + ], + [ + [ + 1082, + 8 + ], + [ + 1084, + 9 + ] + ], + [ + [ + 1126, + 8 + ], + [ + 1129, + 9 + ] + ], + [ + [ + 1183, + 14 + ], + [ + 1185, + 15 + ] + ], + [ + [ + 1209, + 8 + ], + [ + 1211, + 9 + ] + ] + ], + "source": "JS-X-Ray", + "i18n": "sast_warnings.log_usage", + "severity": "Information", + "experimental": false, + "value": "console.warn, console.error", + "file": "cjs/react.development.js" + }, + { + "kind": "log-usage", + "location": [ + [ + [ + 74, + 12 + ], + [ + 76, + 13 + ] + ], + [ + [ + 141, + 10 + ], + [ + 144, + 11 + ] + ], + [ + [ + 156, + 8 + ], + [ + 158, + 9 + ] + ], + [ + [ + 425, + 8 + ], + [ + 427, + 9 + ] + ], + [ + [ + 472, + 12 + ], + [ + 475, + 13 + ] + ], + [ + [ + 477, + 12 + ], + [ + 480, + 13 + ] + ], + [ + [ + 744, + 10 + ], + [ + 746, + 11 + ] + ], + [ + [ + 748, + 12 + ], + [ + 751, + 13 + ] + ], + [ + [ + 754, + 12 + ], + [ + 759, + 13 + ] + ], + [ + [ + 762, + 8 + ], + [ + 764, + 9 + ] + ], + [ + [ + 806, + 8 + ], + [ + 809, + 9 + ] + ], + [ + [ + 375, + 14 + ], + [ + 377, + 15 + ] + ], + [ + [ + 694, + 10 + ], + [ + 696, + 11 + ] + ] + ], + "source": "JS-X-Ray", + "i18n": "sast_warnings.log_usage", + "severity": "Information", + "experimental": false, + "value": "console.error, console.warn", + "file": "cjs/react.react-server.development.js" + }, + { + "kind": "log-usage", + "location": [ + [ + [ + 38, + 12 + ], + [ + 40, + 13 + ] + ], + [ + [ + 132, + 10 + ], + [ + 135, + 11 + ] + ], + [ + [ + 147, + 8 + ], + [ + 149, + 9 + ] + ], + [ + [ + 216, + 12 + ], + [ + 218, + 13 + ] + ], + [ + [ + 232, + 10 + ], + [ + 238, + 11 + ] + ] + ], + "source": "JS-X-Ray", + "i18n": "sast_warnings.log_usage", + "severity": "Information", + "experimental": false, + "value": "console.error", + "file": "cjs/react-jsx-runtime.react-server.development.js" + }, + { + "kind": "log-usage", + "location": [ + [ + [ + 38, + 12 + ], + [ + 40, + 13 + ] + ], + [ + [ + 132, + 10 + ], + [ + 135, + 11 + ] + ], + [ + [ + 147, + 8 + ], + [ + 149, + 9 + ] + ], + [ + [ + 216, + 12 + ], + [ + 218, + 13 + ] + ], + [ + [ + 232, + 10 + ], + [ + 238, + 11 + ] + ] + ], + "source": "JS-X-Ray", + "i18n": "sast_warnings.log_usage", + "severity": "Information", + "experimental": false, + "value": "console.error", + "file": "cjs/react-jsx-runtime.development.js" + }, + { + "kind": "log-usage", + "location": [ + [ + [ + 38, + 12 + ], + [ + 40, + 13 + ] + ], + [ + [ + 132, + 10 + ], + [ + 135, + 11 + ] + ], + [ + [ + 147, + 8 + ], + [ + 149, + 9 + ] + ], + [ + [ + 216, + 12 + ], + [ + 218, + 13 + ] + ], + [ + [ + 232, + 10 + ], + [ + 238, + 11 + ] + ] + ], + "source": "JS-X-Ray", + "i18n": "sast_warnings.log_usage", + "severity": "Information", + "experimental": false, + "value": "console.error", + "file": "cjs/react-jsx-dev-runtime.react-server.development.js" + }, + { + "kind": "log-usage", + "location": [ + [ + [ + 38, + 12 + ], + [ + 40, + 13 + ] + ], + [ + [ + 132, + 10 + ], + [ + 135, + 11 + ] + ], + [ + [ + 147, + 8 + ], + [ + 149, + 9 + ] + ], + [ + [ + 216, + 12 + ], + [ + 218, + 13 + ] + ], + [ + [ + 232, + 10 + ], + [ + 238, + 11 + ] + ] + ], + "source": "JS-X-Ray", + "i18n": "sast_warnings.log_usage", + "severity": "Information", + "experimental": false, + "value": "console.error", + "file": "cjs/react-jsx-dev-runtime.development.js" + }, + { + "kind": "log-usage", + "location": [ + [ + [ + 19, + 8 + ], + [ + 21, + 9 + ] + ] + ], + "source": "JS-X-Ray", + "i18n": "sast_warnings.log_usage", + "severity": "Information", + "experimental": false, + "value": "console.error", + "file": "cjs/react-compiler-runtime.development.js" + } + ], + "dependencyCount": 0, + "gitUrl": null, + "alias": {}, + "description": "React is a JavaScript library for building user interfaces.", + "size": 172628, + "author": null, + "engines": { + "node": ">=0.10.0" + }, + "scripts": {}, + "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": [ + ".js", + "", + ".md", + ".json" + ], + "files": [ + "LICENSE", + "README.md", + "cjs/react-compiler-runtime.development.js", + "cjs/react-compiler-runtime.production.js", + "cjs/react-compiler-runtime.profiling.js", + "cjs/react-jsx-dev-runtime.development.js", + "cjs/react-jsx-dev-runtime.production.js", + "cjs/react-jsx-dev-runtime.profiling.js", + "cjs/react-jsx-dev-runtime.react-server.development.js", + "cjs/react-jsx-dev-runtime.react-server.production.js", + "cjs/react-jsx-runtime.development.js", + "cjs/react-jsx-runtime.production.js", + "cjs/react-jsx-runtime.profiling.js", + "cjs/react-jsx-runtime.react-server.development.js", + "cjs/react-jsx-runtime.react-server.production.js", + "cjs/react.development.js", + "cjs/react.production.js", + "cjs/react.react-server.development.js", + "cjs/react.react-server.production.js", + "compiler-runtime.js", + "index.js", + "jsx-dev-runtime.js", + "jsx-dev-runtime.react-server.js", + "jsx-runtime.js", + "jsx-runtime.react-server.js", + "package.json", + "react.react-server.js" + ], + "minified": [], + "unused": [], + "missing": [ + "react" + ], + "required_files": [ + "cjs/react.production.js", + "cjs/react.development.js", + "cjs/react.react-server.production.js", + "cjs/react.react-server.development.js", + "cjs/react-jsx-runtime.react-server.production.js", + "cjs/react-jsx-runtime.react-server.development.js", + "cjs/react-jsx-runtime.production.js", + "cjs/react-jsx-runtime.development.js", + "cjs/react-jsx-dev-runtime.react-server.production.js", + "cjs/react-jsx-dev-runtime.react-server.development.js", + "cjs/react-jsx-dev-runtime.production.js", + "cjs/react-jsx-dev-runtime.development.js", + "cjs/react-compiler-runtime.production.js", + "cjs/react-compiler-runtime.development.js" + ], + "required_nodejs": [], + "required_thirdparty": [ + "react" + ], + "required_subpath": {} + }, + "repository": { + "type": "git", + "url": "https://github.com/facebook/react.git", + "directory": "packages/react" + }, + "integrity": "834bff9884ee976c5d7bf1045232be43f2c64c36", + "links": { + "npm": "https://www.npmjs.com/package/react/v/19.2.4", + "homepage": "https://react.dev/", + "repository": "https://github.com/facebook/react" + } + } + }, + "vulnerabilities": [], + "metadata": { + "homepage": "https://react.dev/", + "publishedCount": 2760, + "lastVersion": "19.2.4", + "lastUpdateAt": "2026-01-26T18:23:10.244Z", + "hasReceivedUpdateInOneYear": true, + "hasChangedAuthor": false, + "integrity": { + "19.2.4": "834bff9884ee976c5d7bf1045232be43f2c64c36" + }, + "author": { + "name": "jeffbski", + "email": "jeff.barczewski@gmail.com" + }, + "publishers": [ + { + "name": "react-bot", + "email": "react-core@meta.com", + "version": "0.0.0-experimental-3cb2c420-20260324", + "at": "2026-03-25T16:40:02.078Z" + }, + { + "name": "acdlite", + "email": "npm@andrewclark.io", + "version": "0.0.0-experimental-594093496-20230209", + "at": "2023-02-09T16:56:14.956Z" + }, + { + "name": "gnoff", + "email": "jcs.gnoff@gmail.com", + "version": "18.2.0", + "at": "2022-06-14T19:46:38.369Z" + }, + { + "name": "gaearon", + "email": "dan.abramov@gmail.com", + "version": "17.0.2", + "at": "2021-03-22T21:56:19.536Z" + }, + { + "name": "brianvaughn", + "email": "briandavidvaughn@gmail.com", + "version": "0.0.0-experimental-27659559e", + "at": "2021-01-11T16:19:28.958Z" + }, + { + "name": "lunaruan", + "email": "lunaris.ruan@gmail.com", + "version": "0.0.0-experimental-aae83a4b9", + "at": "2020-03-18T01:10:41.870Z" + }, + { + "name": "threepointone", + "email": "threepointone@gmail.com", + "version": "16.13.0", + "at": "2020-02-26T20:19:52.312Z" + }, + { + "name": "clemmy", + "email": "clement.hoang24@gmail.com", + "version": "16.2.0", + "at": "2017-11-28T21:32:28.829Z" + }, + { + "name": "sophiebits", + "email": "npm@sophiebits.com", + "version": "15.6.2", + "at": "2017-09-26T00:10:25.817Z" + }, + { + "name": "flarnie", + "email": "flarnie.npm@gmail.com", + "version": "16.0.0-rc.1", + "at": "2017-09-06T23:11:31.405Z" + }, + { + "name": "spicyj", + "email": "ben@benalpert.com", + "version": "16.0.0-alpha.4", + "at": "2017-03-13T15:57:45.616Z" + }, + { + "name": "tomocchino", + "email": "tomocchino@gmail.com", + "version": "15.4.0", + "at": "2016-11-16T14:33:05.693Z" + }, + { + "name": "sebmarkbage", + "email": "sebastian@calyptus.eu", + "version": "15.4.0-rc.4", + "at": "2016-10-14T22:00:00.474Z" + }, + { + "name": "zpao", + "email": "paul@oshannessy.com", + "version": "15.4.0-rc.2", + "at": "2016-10-05T22:51:07.111Z" + }, + { + "name": "graue", + "email": "scott@oceanbase.org", + "version": "0.14.7", + "at": "2016-01-28T19:59:29.509Z" + }, + { + "name": "jeffmo", + "email": "jeff@anafx.com", + "version": "0.12.1", + "at": "2014-11-18T06:56:11.863Z" + }, + { + "name": "jeffbski", + "email": "jeff.barczewski@gmail.com", + "version": "0.7.1", + "at": "2013-05-23T19:48:26.316Z" + } + ], + "maintainers": [ + { + "name": "fb", + "email": "opensource+npm@fb.com" + }, + { + "name": "react-bot", + "email": "react-core@meta.com" + } + ], + "hasManyPublishers": false + } + } + }, + "metadata": { + "startedAt": 1774601089503, + "executionTime": 771, + "apiCalls": [ + { + "name": "pacote.manifest react@19.2.4", + "startedAt": 1774601089504, + "executionTime": 20 + }, + { + "name": "pacote.extract react@19.2.4", + "startedAt": 1774601089529, + "executionTime": 83 + }, + { + "name": "tarball.scanDirOrArchive react@19.2.4", + "startedAt": 1774601089612, + "executionTime": 247 + } + ], + "apiCallsCount": 3, + "errorCount": 2, + "errors": [ + { + "name": "pacote.extract react@19.2.4" + }, + { + "name": "tarball.scanDirOrArchive react@19.2.4", + "message": "something went wrong !" + } + ] + } +} diff --git a/test/fixtures/result-test4.json b/test/fixtures/result-test4.json new file mode 100644 index 00000000..3ebf234f --- /dev/null +++ b/test/fixtures/result-test4.json @@ -0,0 +1,1020 @@ +{ + "id": "CBuXRl", + "rootDependency": { + "name": "react", + "version": "19.2.4", + "integrity": "sha512-Lfi0RfVh0gdrtD6yI6AInys+HY8Ki8E/SlIpUYm3gXzlKOXW7zvRMMC6sMQ3+3ww+ZSF5cTnOafW2T5HjtwQmg==" + }, + "scannerVersion": "10.7.0", + "vulnerabilityStrategy": "github-advisory", + "warnings": [], + "highlighted": { + "contacts": [], + "packages": [], + "identifiers": [] + }, + "dependencies": { + "react": { + "versions": { + "19.2.4": { + "id": 0, + "type": "cjs", + "usedBy": {}, + "isDevDependency": false, + "existOnRemoteRegistry": true, + "flags": [ + "hasMissingOrUnusedDependency", + "hasWarnings" + ], + "warnings": [ + { + "kind": "log-usage", + "location": [ + [ + [ + 310, + 10 + ], + [ + 310, + 30 + ] + ] + ], + "source": "JS-X-Ray", + "i18n": "sast_warnings.log_usage", + "severity": "Information", + "experimental": false, + "value": "console.error", + "file": "cjs/react.production.js" + }, + { + "kind": "insecure-random", + "location": [ + [ + 529, + 43 + ], + [ + 529, + 56 + ] + ], + "source": "JS-X-Ray", + "i18n": "sast_warnings.insecure_random", + "severity": "Information", + "experimental": false, + "value": null, + "file": "cjs/react.development.js" + }, + { + "kind": "log-usage", + "location": [ + [ + [ + 17, + 10 + ], + [ + 21, + 11 + ] + ], + [ + [ + 413, + 14 + ], + [ + 415, + 15 + ] + ], + [ + [ + 1014, + 10 + ], + [ + 1016, + 11 + ] + ], + [ + [ + 1176, + 12 + ], + [ + 1178, + 13 + ] + ], + [ + [ + 1222, + 8 + ], + [ + 1224, + 9 + ] + ], + [ + [ + 1238, + 8 + ], + [ + 1240, + 9 + ] + ], + [ + [ + 1245, + 8 + ], + [ + 1247, + 9 + ] + ], + [ + [ + 40, + 9 + ], + [ + 44, + 9 + ] + ], + [ + [ + 112, + 12 + ], + [ + 114, + 13 + ] + ], + [ + [ + 179, + 10 + ], + [ + 182, + 11 + ] + ], + [ + [ + 194, + 8 + ], + [ + 196, + 9 + ] + ], + [ + [ + 502, + 12 + ], + [ + 505, + 13 + ] + ], + [ + [ + 507, + 12 + ], + [ + 510, + 13 + ] + ], + [ + [ + 518, + 8 + ], + [ + 520, + 9 + ] + ], + [ + [ + 539, + 16 + ], + [ + 541, + 17 + ] + ], + [ + [ + 556, + 8 + ], + [ + 558, + 9 + ] + ], + [ + [ + 741, + 14 + ], + [ + 741, + 34 + ] + ], + [ + [ + 835, + 12 + ], + [ + 837, + 13 + ] + ], + [ + [ + 890, + 14 + ], + [ + 892, + 15 + ] + ], + [ + [ + 1064, + 10 + ], + [ + 1066, + 11 + ] + ], + [ + [ + 1068, + 12 + ], + [ + 1071, + 13 + ] + ], + [ + [ + 1074, + 12 + ], + [ + 1079, + 13 + ] + ], + [ + [ + 1082, + 8 + ], + [ + 1084, + 9 + ] + ], + [ + [ + 1126, + 8 + ], + [ + 1129, + 9 + ] + ], + [ + [ + 1183, + 14 + ], + [ + 1185, + 15 + ] + ], + [ + [ + 1209, + 8 + ], + [ + 1211, + 9 + ] + ] + ], + "source": "JS-X-Ray", + "i18n": "sast_warnings.log_usage", + "severity": "Information", + "experimental": false, + "value": "console.warn, console.error", + "file": "cjs/react.development.js" + }, + { + "kind": "log-usage", + "location": [ + [ + [ + 74, + 12 + ], + [ + 76, + 13 + ] + ], + [ + [ + 141, + 10 + ], + [ + 144, + 11 + ] + ], + [ + [ + 156, + 8 + ], + [ + 158, + 9 + ] + ], + [ + [ + 425, + 8 + ], + [ + 427, + 9 + ] + ], + [ + [ + 472, + 12 + ], + [ + 475, + 13 + ] + ], + [ + [ + 477, + 12 + ], + [ + 480, + 13 + ] + ], + [ + [ + 744, + 10 + ], + [ + 746, + 11 + ] + ], + [ + [ + 748, + 12 + ], + [ + 751, + 13 + ] + ], + [ + [ + 754, + 12 + ], + [ + 759, + 13 + ] + ], + [ + [ + 762, + 8 + ], + [ + 764, + 9 + ] + ], + [ + [ + 806, + 8 + ], + [ + 809, + 9 + ] + ], + [ + [ + 375, + 14 + ], + [ + 377, + 15 + ] + ], + [ + [ + 694, + 10 + ], + [ + 696, + 11 + ] + ] + ], + "source": "JS-X-Ray", + "i18n": "sast_warnings.log_usage", + "severity": "Information", + "experimental": false, + "value": "console.error, console.warn", + "file": "cjs/react.react-server.development.js" + }, + { + "kind": "log-usage", + "location": [ + [ + [ + 38, + 12 + ], + [ + 40, + 13 + ] + ], + [ + [ + 132, + 10 + ], + [ + 135, + 11 + ] + ], + [ + [ + 147, + 8 + ], + [ + 149, + 9 + ] + ], + [ + [ + 216, + 12 + ], + [ + 218, + 13 + ] + ], + [ + [ + 232, + 10 + ], + [ + 238, + 11 + ] + ] + ], + "source": "JS-X-Ray", + "i18n": "sast_warnings.log_usage", + "severity": "Information", + "experimental": false, + "value": "console.error", + "file": "cjs/react-jsx-runtime.react-server.development.js" + }, + { + "kind": "log-usage", + "location": [ + [ + [ + 38, + 12 + ], + [ + 40, + 13 + ] + ], + [ + [ + 132, + 10 + ], + [ + 135, + 11 + ] + ], + [ + [ + 147, + 8 + ], + [ + 149, + 9 + ] + ], + [ + [ + 216, + 12 + ], + [ + 218, + 13 + ] + ], + [ + [ + 232, + 10 + ], + [ + 238, + 11 + ] + ] + ], + "source": "JS-X-Ray", + "i18n": "sast_warnings.log_usage", + "severity": "Information", + "experimental": false, + "value": "console.error", + "file": "cjs/react-jsx-runtime.development.js" + }, + { + "kind": "log-usage", + "location": [ + [ + [ + 38, + 12 + ], + [ + 40, + 13 + ] + ], + [ + [ + 132, + 10 + ], + [ + 135, + 11 + ] + ], + [ + [ + 147, + 8 + ], + [ + 149, + 9 + ] + ], + [ + [ + 216, + 12 + ], + [ + 218, + 13 + ] + ], + [ + [ + 232, + 10 + ], + [ + 238, + 11 + ] + ] + ], + "source": "JS-X-Ray", + "i18n": "sast_warnings.log_usage", + "severity": "Information", + "experimental": false, + "value": "console.error", + "file": "cjs/react-jsx-dev-runtime.react-server.development.js" + }, + { + "kind": "log-usage", + "location": [ + [ + [ + 38, + 12 + ], + [ + 40, + 13 + ] + ], + [ + [ + 132, + 10 + ], + [ + 135, + 11 + ] + ], + [ + [ + 147, + 8 + ], + [ + 149, + 9 + ] + ], + [ + [ + 216, + 12 + ], + [ + 218, + 13 + ] + ], + [ + [ + 232, + 10 + ], + [ + 238, + 11 + ] + ] + ], + "source": "JS-X-Ray", + "i18n": "sast_warnings.log_usage", + "severity": "Information", + "experimental": false, + "value": "console.error", + "file": "cjs/react-jsx-dev-runtime.development.js" + }, + { + "kind": "log-usage", + "location": [ + [ + [ + 19, + 8 + ], + [ + 21, + 9 + ] + ] + ], + "source": "JS-X-Ray", + "i18n": "sast_warnings.log_usage", + "severity": "Information", + "experimental": false, + "value": "console.error", + "file": "cjs/react-compiler-runtime.development.js" + } + ], + "dependencyCount": 0, + "gitUrl": null, + "alias": {}, + "description": "React is a JavaScript library for building user interfaces.", + "size": 172628, + "author": null, + "engines": { + "node": ">=0.10.0" + }, + "scripts": {}, + "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": [ + ".js", + "", + ".md", + ".json" + ], + "files": [ + "LICENSE", + "README.md", + "cjs/react-compiler-runtime.development.js", + "cjs/react-compiler-runtime.production.js", + "cjs/react-compiler-runtime.profiling.js", + "cjs/react-jsx-dev-runtime.development.js", + "cjs/react-jsx-dev-runtime.production.js", + "cjs/react-jsx-dev-runtime.profiling.js", + "cjs/react-jsx-dev-runtime.react-server.development.js", + "cjs/react-jsx-dev-runtime.react-server.production.js", + "cjs/react-jsx-runtime.development.js", + "cjs/react-jsx-runtime.production.js", + "cjs/react-jsx-runtime.profiling.js", + "cjs/react-jsx-runtime.react-server.development.js", + "cjs/react-jsx-runtime.react-server.production.js", + "cjs/react.development.js", + "cjs/react.production.js", + "cjs/react.react-server.development.js", + "cjs/react.react-server.production.js", + "compiler-runtime.js", + "index.js", + "jsx-dev-runtime.js", + "jsx-dev-runtime.react-server.js", + "jsx-runtime.js", + "jsx-runtime.react-server.js", + "package.json", + "react.react-server.js" + ], + "minified": [], + "unused": [], + "missing": [ + "react" + ], + "required_files": [ + "cjs/react.production.js", + "cjs/react.development.js", + "cjs/react.react-server.production.js", + "cjs/react.react-server.development.js", + "cjs/react-jsx-runtime.react-server.production.js", + "cjs/react-jsx-runtime.react-server.development.js", + "cjs/react-jsx-runtime.production.js", + "cjs/react-jsx-runtime.development.js", + "cjs/react-jsx-dev-runtime.react-server.production.js", + "cjs/react-jsx-dev-runtime.react-server.development.js", + "cjs/react-jsx-dev-runtime.production.js", + "cjs/react-jsx-dev-runtime.development.js", + "cjs/react-compiler-runtime.production.js", + "cjs/react-compiler-runtime.development.js" + ], + "required_nodejs": [], + "required_thirdparty": [ + "react" + ], + "required_subpath": {} + }, + "repository": { + "type": "git", + "url": "https://github.com/facebook/react.git", + "directory": "packages/react" + }, + "integrity": "834bff9884ee976c5d7bf1045232be43f2c64c36", + "links": { + "npm": "https://www.npmjs.com/package/react/v/19.2.4", + "homepage": "https://react.dev/", + "repository": "https://github.com/facebook/react" + } + } + }, + "vulnerabilities": [], + "metadata": { + "homepage": "https://react.dev/", + "publishedCount": 2760, + "lastVersion": "19.2.4", + "lastUpdateAt": "2026-01-26T18:23:10.244Z", + "hasReceivedUpdateInOneYear": true, + "hasChangedAuthor": false, + "integrity": { + "19.2.4": "834bff9884ee976c5d7bf1045232be43f2c64c36" + }, + "author": { + "name": "jeffbski", + "email": "jeff.barczewski@gmail.com" + }, + "publishers": [ + { + "name": "react-bot", + "email": "react-core@meta.com", + "version": "0.0.0-experimental-3cb2c420-20260324", + "at": "2026-03-25T16:40:02.078Z" + }, + { + "name": "acdlite", + "email": "npm@andrewclark.io", + "version": "0.0.0-experimental-594093496-20230209", + "at": "2023-02-09T16:56:14.956Z" + }, + { + "name": "gnoff", + "email": "jcs.gnoff@gmail.com", + "version": "18.2.0", + "at": "2022-06-14T19:46:38.369Z" + }, + { + "name": "gaearon", + "email": "dan.abramov@gmail.com", + "version": "17.0.2", + "at": "2021-03-22T21:56:19.536Z" + }, + { + "name": "brianvaughn", + "email": "briandavidvaughn@gmail.com", + "version": "0.0.0-experimental-27659559e", + "at": "2021-01-11T16:19:28.958Z" + }, + { + "name": "lunaruan", + "email": "lunaris.ruan@gmail.com", + "version": "0.0.0-experimental-aae83a4b9", + "at": "2020-03-18T01:10:41.870Z" + }, + { + "name": "threepointone", + "email": "threepointone@gmail.com", + "version": "16.13.0", + "at": "2020-02-26T20:19:52.312Z" + }, + { + "name": "clemmy", + "email": "clement.hoang24@gmail.com", + "version": "16.2.0", + "at": "2017-11-28T21:32:28.829Z" + }, + { + "name": "sophiebits", + "email": "npm@sophiebits.com", + "version": "15.6.2", + "at": "2017-09-26T00:10:25.817Z" + }, + { + "name": "flarnie", + "email": "flarnie.npm@gmail.com", + "version": "16.0.0-rc.1", + "at": "2017-09-06T23:11:31.405Z" + }, + { + "name": "spicyj", + "email": "ben@benalpert.com", + "version": "16.0.0-alpha.4", + "at": "2017-03-13T15:57:45.616Z" + }, + { + "name": "tomocchino", + "email": "tomocchino@gmail.com", + "version": "15.4.0", + "at": "2016-11-16T14:33:05.693Z" + }, + { + "name": "sebmarkbage", + "email": "sebastian@calyptus.eu", + "version": "15.4.0-rc.4", + "at": "2016-10-14T22:00:00.474Z" + }, + { + "name": "zpao", + "email": "paul@oshannessy.com", + "version": "15.4.0-rc.2", + "at": "2016-10-05T22:51:07.111Z" + }, + { + "name": "graue", + "email": "scott@oceanbase.org", + "version": "0.14.7", + "at": "2016-01-28T19:59:29.509Z" + }, + { + "name": "jeffmo", + "email": "jeff@anafx.com", + "version": "0.12.1", + "at": "2014-11-18T06:56:11.863Z" + }, + { + "name": "jeffbski", + "email": "jeff.barczewski@gmail.com", + "version": "0.7.1", + "at": "2013-05-23T19:48:26.316Z" + } + ], + "maintainers": [ + { + "name": "fb", + "email": "opensource+npm@fb.com" + }, + { + "name": "react-bot", + "email": "react-core@meta.com" + } + ], + "hasManyPublishers": false + } + } + }, + "metadata": { + "startedAt": 1774601089503, + "executionTime": 771, + "apiCalls": [ + { + "name": "pacote.manifest react@19.2.4", + "startedAt": 1774601089504, + "executionTime": 20 + }, + { + "name": "pacote.extract react@19.2.4", + "startedAt": 1774601089529, + "executionTime": 83 + }, + { + "name": "tarball.scanDirOrArchive react@19.2.4", + "startedAt": 1774601089612, + "executionTime": 247 + } + ], + "apiCallsCount": 3, + "errorCount": 0, + "errors": [] + } +}