Skip to content
Open
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
1 change: 1 addition & 0 deletions bin/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ prog
prog
.command("stats")
.describe(i18n.getTokenSync("cli.commands.stats.desc"))
.option("-m, --min", i18n.getTokenSync("cli.commands.stats.option_min"), undefined)
.example("nsecure stats")
.action(commands.stats.main);

Expand Down
6 changes: 6 additions & 0 deletions docs/cli/stats.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,9 @@ The `stats` displays the statistics of the last performed scan such as :
```bash
$ nsecure stats
```

## ⚙️ Available Options

| Name | Shortcut | Default Value | Description |
| ----- | -------- | ------------- | -------------------------------------------------------- |
| `--min` | `-m` | `undefined` | Filter API calls with execution time above ceiling (ms) |
5 changes: 4 additions & 1 deletion i18n/arabic.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,10 @@ const cli = {
elapsed: tS`مدة المسح: ${0}`,
stats: tS`عدد استدعاءات API: ${0}`,
error: "يجب إجراء مسح قبل عرض الإحصائيات.",
errors: tS`عدد الأخطاء: ${0}`
errors: tS`عدد الأخطاء: ${0}`,
option_min: "تصفية استدعاءات API ذات وقت التنفيذ أعلى من الحد المحدد (بالمللي ثانية)",
minNotANumber: "خطأ: يجب أن يكون --min رقماً.",
statsCeiling: tS`عدد استدعاءات API فوق ${0}: ${1}`
}
},
startHttp: {
Expand Down
5 changes: 4 additions & 1 deletion i18n/english.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ const cli = {
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}`
errors: tS`Error count: ${0}`,
option_min: "Filter API calls with execution time above the specified ceiling (in ms)",
minNotANumber: "Error: --min must be a number.",
statsCeiling: tS`API calls count above ${0}: ${1}`
}
},
startHttp: {
Expand Down
5 changes: 4 additions & 1 deletion i18n/french.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ const cli = {
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}`
errors: tS`Nombre d'erreurs: ${0}`,
option_min: "Filtrer les appels API avec un temps d'exécution supérieur au plafond spécifié (en ms)",
minNotANumber: "Erreur: --min doit être un nombre.",
statsCeiling: tS`Nombre d'appels API au-dessus de ${0}: ${1}`
}
},
startHttp: {
Expand Down
5 changes: 4 additions & 1 deletion i18n/turkish.js
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,10 @@ const cli = {
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}`
errors: tS`Hata sayısı: ${0}`,
option_min: "Belirtilen tavan değerinin (ms cinsinden) üzerinde yürütme süresine sahip API çağrılarını filtrele",
minNotANumber: "Hata: --min bir sayı olmalıdır.",
statsCeiling: tS`${0} üzerindeki API çağrıları sayısı: ${1}`
}
},
startHttp: {
Expand Down
19 changes: 17 additions & 2 deletions src/commands/stats.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,36 @@ import path from "node:path";
import { logScannerStat, logScannerError, log, logError, formatMs } from "./loggers/logger.js";

export async function main(options) {
const { getScanResult = getScanFromFile, logger = {
const { getScanResult = getScanFromFile, min, logger = {
logScannerStat,
logScannerError,
log,
logError
} } = options;

if (min !== undefined && typeof min !== "number") {
logger.logError("cli.commands.stats.minNotANumber");

return;
}

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) => {
const apiCallsToLog = min === undefined ?
metadata.apiCalls : metadata.apiCalls.filter(({ executionTime }) => executionTime > min);

if (typeof min === "number" && apiCallsToLog.length !== metadata.apiCallsCount) {
logger.log("cli.commands.stats.statsCeiling", formatMs(min), apiCallsToLog.length);
}

apiCallsToLog.forEach((call) => {
logger.logScannerStat(call, false);
});

if (metadata.errorCount === 0) {
return;
}
Expand Down
86 changes: 86 additions & 0 deletions test/commands/stats.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -115,5 +115,91 @@ describe("stats", () => {
}, false]);
assert.equal(logger.log.mock.calls[2], undefined);
});

test("should filter API calls when min parameter is provided", 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,
min: 50
});

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.log.mock.calls[2].arguments, ["cli.commands.stats.statsCeiling", "50ms", 2]);
assert.deepEqual(logger.logScannerStat.mock.calls.length, 2);
assert.deepEqual(logger.logScannerStat.mock.calls[0].arguments, [{
name: "pacote.extract react@19.2.4",
startedAt: 1774601089529,
executionTime: 83
}, false]);
assert.deepEqual(logger.logScannerStat.mock.calls[1].arguments, [{
name: "tarball.scanDirOrArchive react@19.2.4",
startedAt: 1774601089612,
executionTime: 247
}, false]);
});

test("should not disply the ceiling log if api calls count and api calls count above min are the same", 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,
min: 10
});

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.log.mock.calls[2].arguments, ["cli.commands.stats.errors", 2]);
});

test("should log error when min parameter is not a number", 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,
min: "not-a-number"
});

assert.deepEqual(logger.logError.mock.calls[0].arguments, ["cli.commands.stats.minNotANumber"]);
assert.strictEqual(logger.logScannerStat.mock.callCount(), 0);
assert.strictEqual(logger.log.mock.callCount(), 0);
});
});

Loading