diff --git a/bin/commands/execute.js b/bin/commands/execute.js index 15537e3..d079f78 100644 --- a/bin/commands/execute.js +++ b/bin/commands/execute.js @@ -1,18 +1,61 @@ +// Import Node.js Dependencies +import fs from "node:fs/promises"; + // Import Third-party Dependencies import * as rc from "@nodesecure/rc"; +import kleur from "kleur"; // Import Internal Dependencies import { store } from "../../src/localStorage.js"; -import * as nreport from "../../src/index.js"; + +import { fetchPackagesAndRepositoriesData } from "../../src/analysis/fetch.js"; +import * as CONSTANTS from "../../src/constants.js"; +import * as reporting from "../../src/reporting/index.js"; export async function execute() { - const config = await rc.read( - process.cwd() + const [configResult] = await Promise.all([ + rc.read( + process.cwd() + ), + init() + ]); + + const config = configResult.unwrap(); + const { report } = config; + if (report.reporters.length === 0) { + throw new Error("At least one reporter must be selected (either 'HTML' or 'PDF')"); + } + + console.log(`>> title: ${kleur.cyan().bold(report.title)}`); + console.log(`>> reporters: ${kleur.magenta().bold(report.reporters.join(","))}\n`); + + store.run(config, () => { + fetchPackagesAndRepositoriesData() + .then((data) => reporting.proceed(data)) + .catch((error) => { + console.error(error); + process.exit(0); + }) + .finally(teardown); + }); +} + +function init() { + const directoriesToInitialize = [ + CONSTANTS.DIRS.JSON, + CONSTANTS.DIRS.CLONES, + CONSTANTS.DIRS.REPORTS + ]; + + return Promise.all( + directoriesToInitialize.map((dir) => fs.mkdir(dir, { recursive: true })) ); +} + +function teardown() { + console.log(kleur.green().bold("\n>> Security report successfully generated! Enjoy 🚀.\n")); - store.run(config.unwrap(), () => { - nreport - .execute() - .catch(console.error); + return fs.rm(CONSTANTS.DIRS.CLONES, { + recursive: true, force: true }); } diff --git a/package.json b/package.json index 336627a..fb138d3 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ }, "exports": { ".": { - "import": "./src/api/index.js" + "import": "./src/index.js" } }, "scripts": { diff --git a/src/analysis/fetch.js b/src/analysis/fetch.js index 2f0b5ec..6427147 100644 --- a/src/analysis/fetch.js +++ b/src/analysis/fetch.js @@ -11,7 +11,9 @@ import * as localStorage from "../localStorage.js"; import * as utils from "../utils/index.js"; import * as CONSTANTS from "../constants.js"; -export async function fetchPackagesAndRepositoriesData() { +export async function fetchPackagesAndRepositoriesData( + verbose = true +) { const config = localStorage.getConfig().report; const fetchNpm = config.npm?.packages.length > 0; @@ -27,7 +29,8 @@ export async function fetchPackagesAndRepositoriesData() { utils.formatNpmPackages( config.npm.organizationPrefix, config.npm.packages - ) + ), + verbose ) : null; @@ -35,18 +38,23 @@ export async function fetchPackagesAndRepositoriesData() { const repoStats = fetchGit ? await fetchRepositoriesStats( repositories, - organizationUrl + organizationUrl, + verbose ) : null; return { pkgStats, repoStats }; } -async function fetchPackagesStats(packages) { +async function fetchPackagesStats( + packages, + verbose = true +) { const jsonFiles = await utils.runInSpinner( { title: `[Fetcher: ${kleur.yellow().bold("NPM")}]`, - start: "Fetching NPM packages metadata on the NPM Registry" + start: "Fetching NPM packages metadata on the NPM Registry", + verbose }, async() => Promise.all(packages.map(scanner.from)) ); @@ -56,11 +64,16 @@ async function fetchPackagesStats(packages) { ); } -async function fetchRepositoriesStats(repositories, organizationUrl) { +async function fetchRepositoriesStats( + repositories, + organizationUrl, + verbose = true +) { const jsonFiles = await utils.runInSpinner( { title: `[Fetcher: ${kleur.yellow().bold("GIT")}]`, - start: "Cloning GIT repositories" + start: "Cloning GIT repositories", + verbose }, async(spinner) => { const repos = await Promise.all( diff --git a/src/api/index.js b/src/api/index.js deleted file mode 100644 index ab5b414..0000000 --- a/src/api/index.js +++ /dev/null @@ -1,34 +0,0 @@ -// Import Node.js Dependencies -import fs from "node:fs/promises"; - -// Import Internal Dependencies -import { buildStatsFromNsecurePayloads } from "../analysis/extractScannerData.js"; -import { HTML, PDF } from "../reporting/index.js"; -import * as CONSTANTS from "../constants.js"; - -export async function report( - reportOptions, - scannerPayload -) { - await fs.mkdir( - CONSTANTS.DIRS.REPORTS, - { recursive: true } - ); - - const pkgStats = await buildStatsFromNsecurePayloads(scannerPayload, { - isJson: true, - reportOptions - }); - const fakeSpinner = Object.create(null); - - const reportHTMLPath = await HTML({ - pkgStats, - repoStats: null, - spinner: fakeSpinner - }, reportOptions); - - return PDF(reportHTMLPath, { - title: reportOptions.title, - saveOnDisk: false - }); -} diff --git a/src/api/report.js b/src/api/report.js new file mode 100644 index 0000000..8e8a300 --- /dev/null +++ b/src/api/report.js @@ -0,0 +1,37 @@ +// Import Node.js Dependencies +import path from "node:path"; +import os from "node:os"; +import fs from "node:fs/promises"; + +// Import Internal Dependencies +import { buildStatsFromNsecurePayloads } from "../analysis/extractScannerData.js"; +import { HTML, PDF } from "../reporting/index.js"; + +export async function report( + reportOptions, + scannerPayload +) { + const [pkgStats, reportOutputLocation] = await Promise.all([ + buildStatsFromNsecurePayloads(scannerPayload, { + isJson: true, + reportOptions + }), + fs.mkdtemp( + path.join(os.tmpdir(), "nsecure-report-") + ) + ]); + + const reportHTMLPath = await HTML( + { + pkgStats, + repoStats: null + }, + reportOptions, + reportOutputLocation + ); + + return PDF(reportHTMLPath, { + title: reportOptions.title, + saveOnDisk: false + }); +} diff --git a/src/index.js b/src/index.js index 2a4acb4..979ba58 100644 --- a/src/index.js +++ b/src/index.js @@ -1,55 +1 @@ -// Import Node.js Dependencies -import fs from "node:fs/promises"; - -// Import Third-party Dependencies -import kleur from "kleur"; - -// Import Internal Dependencies -import { fetchPackagesAndRepositoriesData } from "./analysis/fetch.js"; -import * as localStorage from "./localStorage.js"; -import * as CONSTANTS from "./constants.js"; -import * as reporting from "./reporting/index.js"; - -export async function execute(options = Object.create(null)) { - const { exitOnError = true } = options; - - try { - await prepareExecute(); - - const config = localStorage.getConfig().report; - if (config.reporters.length === 0) { - throw new Error("At least one reporter must be selected (either 'HTML' or 'PDF')"); - } - - console.log(`>> ${kleur.cyan().bold(config.title)}`); - console.log(`>> reporters: ${kleur.magenta().bold(config.reporters.join(","))}\n`); - - const data = await fetchPackagesAndRepositoriesData(); - await reporting.proceed(data); - - console.log(kleur.green().bold("\n>> Security report successfully generated! Enjoy 🚀.\n")); - } - catch (error) { - console.log(kleur.bold().red(error.message)); - - if (!exitOnError) { - throw error; - } - setImmediate(() => process.exit(0)); - } - finally { - await fs.rm(CONSTANTS.DIRS.CLONES, { - recursive: true, force: true - }); - } -} - -async function prepareExecute() { - const directoriesToInitialize = [ - CONSTANTS.DIRS.JSON, CONSTANTS.DIRS.CLONES, CONSTANTS.DIRS.REPORTS - ]; - - await Promise.all( - directoriesToInitialize.map((dir) => fs.mkdir(dir, { recursive: true })) - ); -} +export * from "./api/report.js"; diff --git a/src/reporting/html.js b/src/reporting/html.js index 5460af2..6550b61 100644 --- a/src/reporting/html.js +++ b/src/reporting/html.js @@ -4,7 +4,6 @@ import { readdirSync, promises as fs } from "node:fs"; // Import Third-party Dependencies import esbuild from "esbuild"; -import kleur from "kleur"; // Import Internal Dependencies import * as utils from "../utils/index.js"; @@ -52,7 +51,7 @@ export async function HTML( reportOptions = null, reportOutputLocation = CONSTANTS.DIRS.REPORTS ) { - const { pkgStats, repoStats, spinner } = data; + const { pkgStats, repoStats } = data; const config = reportOptions ?? localStorage.getConfig().report; const assetsOutputLocation = path.join(reportOutputLocation, "..", "dist"); @@ -62,7 +61,6 @@ export async function HTML( utils.cleanReportName(config.title, ".html") ); - spinner.text = "Building view with zup"; const charts = config.charts .flatMap(({ display, name, help = null }) => (display ? [{ name, help }] : [])); @@ -78,16 +76,16 @@ export async function HTML( } ).render(); - await fs.writeFile( - reportFinalOutputLocation, - HTMLReport - ); - - spinner.text = kleur.yellow().bold("Bundling assets with esbuild"); - await buildFrontAssets( - assetsOutputLocation, - { theme: reportTheme } - ); + await Promise.all([ + fs.writeFile( + reportFinalOutputLocation, + HTMLReport + ), + buildFrontAssets( + assetsOutputLocation, + { theme: reportTheme } + ) + ]); return reportFinalOutputLocation; } diff --git a/src/reporting/index.js b/src/reporting/index.js index 28abf20..f6edc5b 100644 --- a/src/reporting/index.js +++ b/src/reporting/index.js @@ -10,13 +10,16 @@ import { HTML } from "./html.js"; import { PDF } from "./pdf.js"; export async function proceed( - data + data, + verbose = true ) { const reportHTMLPath = await utils.runInSpinner( { - title: `[Reporter: ${kleur.yellow().bold("HTML")}]` + title: `[Reporter: ${kleur.yellow().bold("HTML")}]`, + start: "Building template and assets", + verbose }, - async(spinner) => HTML({ ...data, spinner }) + async() => HTML(data) ); const { reporters, title } = localStorage.getConfig().report; @@ -27,7 +30,8 @@ export async function proceed( await utils.runInSpinner( { title: `[Reporter: ${kleur.yellow().bold("PDF")}]`, - start: "Using puppeteer to convert HTML content to PDF" + start: "Using puppeteer to convert HTML content to PDF", + verbose }, async() => PDF(reportHTMLPath, { title }) );