From f1258f85b28d9a5d383a36a0c5737e68837fde0a Mon Sep 17 00:00:00 2001 From: fraxken Date: Fri, 29 Nov 2024 20:49:17 +0100 Subject: [PATCH 1/2] chore: use @openally/config.eslint --- eslint.config.mjs | 31 ++++++++++--------------------- package.json | 4 ++-- 2 files changed, 12 insertions(+), 23 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index be7128f..b2e4ed3 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,25 +1,14 @@ -// Import Node.js Dependencies -import path from "node:path"; -import { fileURLToPath } from "node:url"; +import { ESLintConfig } from "@openally/config.eslint"; -// Import Third-party Dependencies -import { FlatCompat } from "@eslint/eslintrc"; +export default [ + ...ESLintConfig, + { + languageOptions: { + sourceType: "module", -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const compat = new FlatCompat({ - baseDirectory: __dirname -}); - -export default [{ - ignores: ["**/node_modules/", "**/tmp/", "**/dist/", "**/coverage/", "**/fixtures/", "**/public/lib"] -}, ...compat.extends("@nodesecure/eslint-config"), { - languageOptions: { - sourceType: "module", - - parserOptions: { - requireConfigFile: false + parserOptions: { + requireConfigFile: false + } } } -}]; +]; diff --git a/package.json b/package.json index 1517252..548eaad 100644 --- a/package.json +++ b/package.json @@ -60,10 +60,10 @@ "zup": "0.0.2" }, "devDependencies": { - "@nodesecure/eslint-config": "2.0.0-beta.0", + "@openally/config.eslint": "^1.1.0", + "@openally/config.typescript": "^1.0.3", "@types/node": "^22.2.0", "c8": "^10.1.2", - "eslint": "^9.8.0", "glob": "^11.0.0", "open": "^10.1.0" }, From 3ae728aa545636eec2ae40c10adcb65933e62d1c Mon Sep 17 00:00:00 2001 From: fraxken Date: Sat, 21 Dec 2024 17:00:42 +0100 Subject: [PATCH 2/2] refactor: migrate to TypeScript --- README.md | 3 +- bin/commands/{execute.js => execute.ts} | 15 ++- bin/commands/{index.js => index.ts} | 0 bin/commands/{init.js => init.ts} | 0 bin/{index.js => index.ts} | 2 +- eslint.config.mjs | 17 +-- package.json | 28 ++-- public/lib/md5.js | 1 - public/scripts/main.js | 2 + scripts/{preview.js => preview.ts} | 25 ++-- ...ctScannerData.js => extractScannerData.ts} | 123 ++++++++++++------ src/analysis/{fetch.js => fetch.ts} | 32 ++--- src/analysis/{scanner.js => scanner.ts} | 30 ++--- src/api/{report.js => report.ts} | 54 +++++--- src/{constants.js => constants.ts} | 0 src/{index.js => index.ts} | 0 src/localStorage.js | 8 -- src/localStorage.ts | 16 +++ src/reporting/{html.js => html.ts} | 32 +++-- src/reporting/{index.js => index.ts} | 8 +- src/reporting/{pdf.js => pdf.ts} | 16 ++- src/reporting/{template.js => template.ts} | 30 ++++- src/utils/charts.js | 38 ------ src/utils/charts.ts | 63 +++++++++ ...{cleanReportName.js => cleanReportName.ts} | 13 +- ...GITRepository.js => cloneGITRepository.ts} | 15 +-- ...matNpmPackages.js => formatNpmPackages.ts} | 12 +- src/utils/{index.js => index.ts} | 0 .../{runInSpinner.js => runInSpinner.ts} | 18 ++- test/api/{report.spec.js => report.spec.ts} | 32 +++-- ...rtName.spec.js => cleanReportName.spec.ts} | 0 ...ory.spec.js => cloneGITRepository.spec.ts} | 0 ...ages.spec.js => formatNpmPackages.spec.ts} | 0 tsconfig.json | 12 ++ 34 files changed, 388 insertions(+), 257 deletions(-) rename bin/commands/{execute.js => execute.ts} (87%) rename bin/commands/{index.js => index.ts} (100%) rename bin/commands/{init.js => init.ts} (100%) rename bin/{index.js => index.ts} (92%) mode change 100755 => 100644 rename scripts/{preview.js => preview.ts} (82%) rename src/analysis/{extractScannerData.js => extractScannerData.ts} (63%) rename src/analysis/{fetch.js => fetch.ts} (76%) rename src/analysis/{scanner.js => scanner.ts} (71%) rename src/api/{report.js => report.ts} (57%) rename src/{constants.js => constants.ts} (100%) rename src/{index.js => index.ts} (100%) delete mode 100644 src/localStorage.js create mode 100644 src/localStorage.ts rename src/reporting/{html.js => html.ts} (79%) rename src/reporting/{index.js => index.ts} (82%) rename src/reporting/{pdf.js => pdf.ts} (79%) rename src/reporting/{template.js => template.ts} (61%) delete mode 100644 src/utils/charts.js create mode 100644 src/utils/charts.ts rename src/utils/{cleanReportName.js => cleanReportName.ts} (65%) rename src/utils/{cloneGITRepository.js => cloneGITRepository.ts} (57%) rename src/utils/{formatNpmPackages.js => formatNpmPackages.ts} (66%) rename src/utils/{index.js => index.ts} (100%) rename src/utils/{runInSpinner.js => runInSpinner.ts} (64%) rename test/api/{report.spec.js => report.spec.ts} (88%) rename test/utils/{cleanReportName.spec.js => cleanReportName.spec.ts} (100%) rename test/utils/{cloneGITRepository.spec.js => cloneGITRepository.spec.ts} (100%) rename test/utils/{formatNpmPackages.spec.js => formatNpmPackages.spec.ts} (100%) create mode 100644 tsconfig.json diff --git a/README.md b/README.md index 4583af1..e9468f4 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ This package is available in the Node Package Repository and can be easily insta $ git clone https://github.com/NodeSecure/report.git $ cd report $ npm i +$ npm run build $ npm link ``` @@ -72,7 +73,7 @@ This uses the official NodeSecure [runtime configuration](https://github.com/Nod { "version": "1.0.0", "i18n": "english", - "strategy": "npm", + "strategy": "github-advisory", "report": { "title": "NodeSecure Security Report", "logoUrl": "https://avatars.githubusercontent.com/u/85318671?s=200&v=4", diff --git a/bin/commands/execute.js b/bin/commands/execute.ts similarity index 87% rename from bin/commands/execute.js rename to bin/commands/execute.ts index 37b6d40..2788c4b 100644 --- a/bin/commands/execute.js +++ b/bin/commands/execute.ts @@ -19,9 +19,13 @@ import * as reporting from "../../src/reporting/index.js"; const kReadConfigOptions = { createIfDoesNotExist: false, createMode: "report" -}; +} as const; -export async function execute(options = {}) { +export interface ExecuteOptions { + debug?: boolean; +} + +export async function execute(options: ExecuteOptions = {}) { const { debug: debugMode } = options; if (debugMode) { @@ -36,7 +40,10 @@ export async function execute(options = {}) { const config = configResult.unwrap(); const { report } = config; - if (report.reporters.length === 0) { + if (!report) { + throw new Error("A valid configuration is required"); + } + if (!report.reporters || report.reporters.length === 0) { throw new Error("At least one reporter must be selected (either 'HTML' or 'PDF')"); } @@ -75,7 +82,7 @@ function init() { ); } -function debug(obj) { +function debug(obj: any) { const filePath = path.join(CONSTANTS.DIRS.REPORTS, `debug-pkg-repo.txt`); writeFileSync(filePath, inspect(obj, { showHidden: true, depth: null }), "utf8"); } diff --git a/bin/commands/index.js b/bin/commands/index.ts similarity index 100% rename from bin/commands/index.js rename to bin/commands/index.ts diff --git a/bin/commands/init.js b/bin/commands/init.ts similarity index 100% rename from bin/commands/init.js rename to bin/commands/init.ts diff --git a/bin/index.js b/bin/index.ts old mode 100755 new mode 100644 similarity index 92% rename from bin/index.js rename to bin/index.ts index 86e9de9..5c55714 --- a/bin/index.js +++ b/bin/index.ts @@ -13,7 +13,7 @@ import * as commands from "./commands/index.js"; console.log(kleur.grey().bold(`\n > Executing nreport at: ${kleur.yellow().bold(process.cwd())}\n`)); const { version } = JSON.parse( - fs.readFileSync(new URL("../package.json", import.meta.url)) + fs.readFileSync(new URL("../package.json", import.meta.url), "utf-8") ); const cli = sade("nreport").version(version); diff --git a/eslint.config.mjs b/eslint.config.mjs index b2e4ed3..85c3ebb 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,14 +1,9 @@ -import { ESLintConfig } from "@openally/config.eslint"; +import { typescriptConfig, globals } from "@openally/config.eslint"; -export default [ - ...ESLintConfig, - { - languageOptions: { - sourceType: "module", - - parserOptions: { - requireConfigFile: false - } +export default typescriptConfig({ + languageOptions: { + globals: { + ...globals.browser } } -]; +}); diff --git a/package.json b/package.json index 548eaad..c42cc71 100644 --- a/package.json +++ b/package.json @@ -2,28 +2,29 @@ "name": "@nodesecure/report", "version": "3.0.0", "description": "NodeSecure HTML & PDF graphic security report", - "main": "./bin/index.js", + "main": "./dist/src/index.js", "type": "module", "bin": { - "nreport": "./bin/index.js" + "nreport": "./dist/bin/index.js" }, "exports": { ".": { - "import": "./src/index.js" + "import": "./dist/src/index.js" } }, "scripts": { - "lint": "eslint src test", - "test-only": "glob -c \"node --test-reporter=spec --test\" \"./test/**/*.spec.js\"", + "build": "tsc && npm run build:views && npm run build:public", + "build:views": "rimraf dist/views && cp -r views dist/views", + "build:public": "rimraf dist/public && cp -r public dist/public", + "lint": "eslint src test bin scripts", + "test-only": "glob -c \"tsx --test-reporter=spec --test\" \"./test/**/*.spec.ts\"", "test": "c8 --all --src ./src -r html npm run test-only", - "preview:light": "node --no-warnings ./scripts/preview.js --theme light", - "preview:dark": "node --no-warnings ./scripts/preview.js --theme dark" + "preview:light": "tsx --no-warnings ./scripts/preview.js --theme light", + "preview:dark": "tsx --no-warnings ./scripts/preview.js --theme dark", + "prepublishOnly": "npm run build" }, "files": [ - "bin", - "public", - "src", - "views" + "dist" ], "repository": { "type": "git", @@ -53,7 +54,6 @@ "@topcli/spinner": "^2.1.2", "esbuild": "^0.24.0", "filenamify": "^6.0.0", - "frequency-set": "^1.0.2", "kleur": "^4.1.5", "puppeteer": "23.6.0", "sade": "^1.8.1", @@ -65,7 +65,9 @@ "@types/node": "^22.2.0", "c8": "^10.1.2", "glob": "^11.0.0", - "open": "^10.1.0" + "open": "^10.1.0", + "tsx": "^4.19.2", + "typescript": "^5.7.2" }, "engines": { "node": ">=20" diff --git a/public/lib/md5.js b/public/lib/md5.js index 90ba00e..7bf6394 100644 --- a/public/lib/md5.js +++ b/public/lib/md5.js @@ -98,7 +98,6 @@ function ii(a, b, c, d, x, s, t) { } function md51(s) { - txt = ''; var n = s.length, state = [1732584193, -271733879, -1732584194, 271733878], i; for (i = 64; i <= s.length; i += 64) { diff --git a/public/scripts/main.js b/public/scripts/main.js index 4f83b15..b9863d3 100644 --- a/public/scripts/main.js +++ b/public/scripts/main.js @@ -1,3 +1,5 @@ +/* eslint-disable no-undef */ + // Import Internal Dependencies import { md5 } from "../lib/md5.js"; diff --git a/scripts/preview.js b/scripts/preview.ts similarity index 82% rename from scripts/preview.js rename to scripts/preview.ts index cfd22d5..a796622 100644 --- a/scripts/preview.js +++ b/scripts/preview.ts @@ -43,12 +43,12 @@ const payload = (await import( payload.report_theme = theme; const config = { - theme, + theme: theme as ("light" | "dark"), includeTransitiveInternal: false, - reporters: [ "html" ], + reporters: ["html" as const], npm: { organizationPrefix: "@nodesecure", - packages: [ "@nodesecure/js-x-ray" ] + packages: ["@nodesecure/js-x-ray"] }, git: { organizationUrl: "https://github.com/NodeSecure", @@ -56,27 +56,27 @@ const config = { }, charts: [ { - name: "Extensions", + name: "Extensions" as const, display: true, interpolation: "d3.interpolateRainbow", - type: "bar" + type: "bar" as const }, { - name: "Licenses", + name: "Licenses" as const, display: true, interpolation: "d3.interpolateCool", - type: "bar" + type: "bar" as const }, { - name: "Warnings", + name: "Warnings" as const, display: true, - type: "horizontalBar", + type: "horizontalBar" as const, interpolation: "d3.interpolateInferno" }, { - name: "Flags", + name: "Flags" as const, display: true, - type: "horizontalBar", + type: "horizontalBar" as const, interpolation: "d3.interpolateSinebow" } ], @@ -86,7 +86,8 @@ const config = { }; const HTMLReport = new HTMLTemplateGenerator( - payload, config + payload, + config ).render({ asset_location: "./dist" }); const previewLocation = path.join(kPreviewDir, "preview.html"); diff --git a/src/analysis/extractScannerData.js b/src/analysis/extractScannerData.ts similarity index 63% rename from src/analysis/extractScannerData.js rename to src/analysis/extractScannerData.ts index 77623e6..14ee9ff 100644 --- a/src/analysis/extractScannerData.js +++ b/src/analysis/extractScannerData.ts @@ -4,39 +4,75 @@ import fs from "node:fs"; // Import Third-party Dependencies import { formatBytes, getScoreColor, getVCSRepositoryPathAndPlatform } from "@nodesecure/utils"; -import * as Flags from "@nodesecure/flags"; +import { getManifest, getFlags } from "@nodesecure/flags/web"; import * as scorecard from "@nodesecure/ossf-scorecard-sdk"; +import type { Payload } from "@nodesecure/scanner"; +import type { RC } from "@nodesecure/rc"; // Import Internal Dependencies import * as localStorage from "../localStorage.js"; // CONSTANTS -const kFlagsList = Object.values(Flags.getManifest()); -const kWantedFlags = Flags.getFlags(); +const kFlagsList = Object.values(getManifest()); +const kWantedFlags = getFlags(); const kScorecardVisualizerUrl = `https://kooltheba.github.io/openssf-scorecard-api-visualizer/#/projects`; const kNodeVisualizerUrl = `https://nodejs.org/dist/latest/docs/api`; -function splitPackageWithOrg(pkg) { +function splitPackageWithOrg(pkg: string) { // reverse here so if there is no orgPrefix, its value will be undefined const [name, orgPrefix] = pkg.split("/").reverse(); return { orgPrefix, name }; } -/** - * - * @param {string[] | NodeSecure.Payload | NodeSecure.Payload[]} payloadFiles - * @param {object} options - * @param {boolean} options.isJson - * @returns - */ -export async function buildStatsFromNsecurePayloads(payloadFiles = [], options = Object.create(null)) { - const { isJson = false, reportConfig } = options; - - const config = reportConfig ?? localStorage.getConfig().report; - const stats = { +export interface ReportStat { + size: { + all: string; + internal: string; + external: string; + }; + deps: { + transitive: Record; + node: Record; + }; + licenses: Record; + flags: Record; + flagsList: any; + extensions: Record; + warnings: Record; + authors: Record; + packages: Record; + packages_count: { + all: number; + internal: number; + external: number; + }; + scorecards: Record; + showFlags: boolean; +} + +export interface BuildScannerStatsOptions { + reportConfig?: RC["report"]; +} + +export async function buildStatsFromScannerDependencies( + payloadFiles: string[] | Payload["dependencies"] = [], + options: BuildScannerStatsOptions = Object.create(null) +): Promise { + const { reportConfig } = options; + + const config = reportConfig ?? localStorage.getConfig().report!; + const sizeStats = { + all: 0, + internal: 0, + external: 0 + }; + + const stats: ReportStat = { size: { - all: 0, internal: 0, external: 0 + all: "", + internal: "", + external: "" }, deps: { transitive: {}, @@ -55,33 +91,40 @@ export async function buildStatsFromNsecurePayloads(payloadFiles = [], options = all: 0, internal: 0, external: 0 }, scorecards: {}, - showFlags: config.showFlags + showFlags: config.showFlags ?? true }; - /** - * @param {string | NodeSecure.Payload} fileOrJson - * @returns {NodeSecure.Payload} - */ - function getJSONPayload(fileOrJson) { - if (isJson) { - return fileOrJson; - } + function getPayloadDependencies( + fileOrJson: string | Payload["dependencies"] + ): Payload["dependencies"] { + if (typeof fileOrJson === "string") { + const buf = fs.readFileSync(fileOrJson); + const dependencies = JSON.parse( + buf.toString() + ) as Payload["dependencies"]; - const buf = fs.readFileSync(fileOrJson); + return dependencies; + } - return JSON.parse(buf.toString()); + return fileOrJson; } const payloads = Array.isArray(payloadFiles) ? payloadFiles : [payloadFiles]; + const npmConfig = config.npm!; for (const fileOrJson of payloads) { - const nsecurePayload = getJSONPayload(fileOrJson); + const dependencies = getPayloadDependencies(fileOrJson); - for (const [name, descriptor] of Object.entries(nsecurePayload)) { + for (const [name, descriptor] of Object.entries(dependencies)) { const { versions, metadata } = descriptor; - const isThird = config.npm.organizationPrefix === null ? true : !name.startsWith(`${config.npm.organizationPrefix}/`); + const isThird = npmConfig.organizationPrefix === null ? + true : + !name.startsWith(`${npmConfig.organizationPrefix}/`); for (const human of metadata.maintainers) { - stats.authors[human.email] = human.email in stats.authors ? ++stats.authors[human.email] : 1; + if (human.email) { + stats.authors[human.email] = human.email in stats.authors ? + ++stats.authors[human.email] : 1; + } } if (!(name in stats.packages)) { @@ -100,22 +143,22 @@ export async function buildStatsFromNsecurePayloads(payloadFiles = [], options = } const { flags, size, composition, uniqueLicenseIds, author, warnings = [], links = [] } = localDescriptor; - stats.size.all += size; - stats.size[isThird ? "external" : "internal"] += size; + sizeStats.all += size; + sizeStats[isThird ? "external" : "internal"] += size; for (const { kind } of warnings) { stats.warnings[kind] = kind in stats.warnings ? ++stats.warnings[kind] : 1; } for (const flag of flags) { - if (!kWantedFlags.has(flag)) { + if (!(flag in kWantedFlags)) { continue; } stats.flags[flag] = flag in stats.flags ? ++stats.flags[flag] : 1; stats.packages[name].flags[flag] = { ...stats.flagsList[flag] }; } - (composition.required_builtin || composition.required_nodejs) + (composition.required_nodejs) .forEach((dep) => (stats.deps.node[dep] = { visualizerUrl: `${kNodeVisualizerUrl}/${dep.replace("node:", "")}.html` })); for (const extName of composition.extensions.filter((extName) => extName !== "")) { stats.extensions[extName] = extName in stats.extensions ? ++stats.extensions[extName] : 1; @@ -134,7 +177,7 @@ export async function buildStatsFromNsecurePayloads(payloadFiles = [], options = curr.versions.add(localVersion); const hasIndirectDependencies = flags.includes("hasIndirectDependencies"); id: if (hasIndirectDependencies) { - if (!config.includeTransitiveInternal && name.startsWith(config.npm.organizationPrefix)) { + if (!config.includeTransitiveInternal && name.startsWith(npmConfig.organizationPrefix)) { break id; } @@ -164,9 +207,9 @@ export async function buildStatsFromNsecurePayloads(payloadFiles = [], options = stats.packages_count.all = Object.keys(stats.packages).length; stats.packages_count.internal = stats.packages_count.all - stats.packages_count.external; - stats.size.all = formatBytes(stats.size.all); - stats.size.internal = formatBytes(stats.size.internal); - stats.size.external = formatBytes(stats.size.external); + stats.size.all = formatBytes(sizeStats.all); + stats.size.internal = formatBytes(sizeStats.internal); + stats.size.external = formatBytes(sizeStats.external); return stats; } diff --git a/src/analysis/fetch.js b/src/analysis/fetch.ts similarity index 76% rename from src/analysis/fetch.js rename to src/analysis/fetch.ts index 77cd276..49ddae1 100644 --- a/src/analysis/fetch.js +++ b/src/analysis/fetch.ts @@ -5,7 +5,7 @@ import path from "node:path"; import kleur from "kleur"; // Import Internal Dependencies -import { buildStatsFromNsecurePayloads } from "./extractScannerData.js"; +import { buildStatsFromScannerDependencies } from "./extractScannerData.js"; import * as scanner from "./scanner.js"; import * as localStorage from "../localStorage.js"; import * as utils from "../utils/index.js"; @@ -14,17 +14,17 @@ import * as CONSTANTS from "../constants.js"; export async function fetchPackagesAndRepositoriesData( verbose = true ) { - const config = localStorage.getConfig().report; + const config = localStorage.getConfig().report!; - const fetchNpm = config.npm?.packages.length > 0; - const fetchGit = config.git?.repositories.length > 0; + const fetchNpm = (config.npm?.packages ?? []).length > 0; + const fetchGit = (config.git?.repositories ?? []).length > 0; if (!fetchGit && !fetchNpm) { throw new Error( "No git repositories and no npm packages to fetch in the local configuration!" ); } - const pkgStats = fetchNpm ? + const pkgStats = fetchNpm && config.npm ? await fetchPackagesStats( utils.formatNpmPackages( config.npm.organizationPrefix, @@ -34,20 +34,22 @@ export async function fetchPackagesAndRepositoriesData( ) : null; - const { repositories, organizationUrl } = config.git; - const repoStats = fetchGit ? + const repoStats = fetchGit && config.git ? await fetchRepositoriesStats( - repositories, - organizationUrl, + config.git.repositories, + config.git.organizationUrl, verbose ) : null; - return { pkgStats, repoStats }; + return { + pkgStats, + repoStats + }; } async function fetchPackagesStats( - packages, + packages: string[], verbose = true ) { const jsonFiles = await utils.runInSpinner( @@ -59,14 +61,14 @@ async function fetchPackagesStats( async() => Promise.all(packages.map(scanner.from)) ); - return buildStatsFromNsecurePayloads( + return buildStatsFromScannerDependencies( jsonFiles.filter((value) => value !== null) ); } async function fetchRepositoriesStats( - repositories, - organizationUrl, + repositories: string[], + organizationUrl: string, verbose = true ) { const jsonFiles = await utils.runInSpinner( @@ -92,7 +94,7 @@ async function fetchRepositoriesStats( } ); - return buildStatsFromNsecurePayloads( + return buildStatsFromScannerDependencies( jsonFiles.filter((value) => value !== null) ); } diff --git a/src/analysis/scanner.js b/src/analysis/scanner.ts similarity index 71% rename from src/analysis/scanner.js rename to src/analysis/scanner.ts index 956f7e5..fe77197 100644 --- a/src/analysis/scanner.js +++ b/src/analysis/scanner.ts @@ -12,20 +12,16 @@ import * as CONSTANTS from "../constants.js"; // CONSTANTS const kMaxAnalysisLock = new Mutex({ concurrency: 2 }); -/** - * @async - * @function from - * @description run nsecure on a given npm package (on the npm registry). - * @param {!string} packageName - * @returns {Promise} - */ -export async function from(packageName) { +export async function from( + packageName: string +): Promise { const release = await kMaxAnalysisLock.acquire(); try { const name = `${packageName}.json`; const { dependencies } = await scanner.from(packageName, { - maxDepth: 4, verbose: false + maxDepth: 4, + vulnerabilityStrategy: "none" }); const filePath = path.join(CONSTANTS.DIRS.JSON, name); @@ -34,7 +30,7 @@ export async function from(packageName) { return filePath; } - catch (error) { + catch { return null; } finally { @@ -42,20 +38,16 @@ export async function from(packageName) { } } -/** - * @async - * @function cwd - * @description run nsecure on a local directory - * @param {!string} dir - * @returns {Promise} - */ -export async function cwd(dir) { +export async function cwd( + dir: string +): Promise { const release = await kMaxAnalysisLock.acquire(); try { const name = `${path.basename(dir)}.json`; const { dependencies } = await scanner.cwd(dir, { - maxDepth: 4, verbose: false, usePackageLock: false + maxDepth: 4, + vulnerabilityStrategy: "none" }); const filePath = path.join(CONSTANTS.DIRS.JSON, name); diff --git a/src/api/report.js b/src/api/report.ts similarity index 57% rename from src/api/report.js rename to src/api/report.ts index 4d3c962..1cec85f 100644 --- a/src/api/report.js +++ b/src/api/report.ts @@ -1,22 +1,29 @@ // Import Node.js Dependencies -import path from "node:path"; -import os from "node:os"; -import fs from "node:fs/promises"; +import * as path from "node:path"; +import * as os from "node:os"; +import * as fs from "node:fs/promises"; + +// Import Third-party Dependencies +import { type Payload } from "@nodesecure/scanner"; +import { type RC } from "@nodesecure/rc"; // Import Internal Dependencies -import { buildStatsFromNsecurePayloads } from "../analysis/extractScannerData.js"; +import { buildStatsFromScannerDependencies } from "../analysis/extractScannerData.js"; import { HTML, PDF } from "../reporting/index.js"; +export interface ReportLocationOptions { + includesPDF: boolean; + savePDFOnDisk: boolean; + saveHTMLOnDisk: boolean; +} + /** * Determine the final location of the report (on current working directory or in a temporary directory) - * @param {string} location - * @param {object} options - * @param {boolean} options.includesPDF - * @param {boolean} options.savePDFOnDisk - * @param {boolean} options.saveHTMLOnDisk - * @returns {Promise} */ -async function reportLocation(location, options) { +async function reportLocation( + location: string | null, + options: ReportLocationOptions +): Promise { const { includesPDF, savePDFOnDisk, @@ -34,31 +41,36 @@ async function reportLocation(location, options) { return fs.mkdtemp(path.join(os.tmpdir(), "nsecure-report-")); } +export interface ReportOptions { + reportOutputLocation?: string; + savePDFOnDisk?: boolean; + saveHTMLOnDisk?: boolean; +} + export async function report( - scannerDependencies, - reportConfig, - reportOptions = Object.create(null) -) { + scannerDependencies: Payload["dependencies"], + reportConfig: NonNullable, + reportOptions: ReportOptions = Object.create(null) +): Promise { const { reportOutputLocation = null, savePDFOnDisk = false, saveHTMLOnDisk = false } = reportOptions; - const includesPDF = reportConfig.reporters.includes("pdf"); - const includesHTML = reportConfig.reporters.includes("html"); + const includesPDF = reportConfig.reporters?.includes("pdf") ?? false; + const includesHTML = reportConfig.reporters?.includes("html") ?? false; if (!includesPDF && !includesHTML) { throw new Error("At least one reporter must be enabled (pdf or html)"); } const [pkgStats, finalReportLocation] = await Promise.all([ - buildStatsFromNsecurePayloads(scannerDependencies, { - isJson: true, + buildStatsFromScannerDependencies(scannerDependencies, { reportConfig }), reportLocation(reportOutputLocation, { includesPDF, savePDFOnDisk, saveHTMLOnDisk }) ]); - let reportHTMLPath; + let reportHTMLPath: string | undefined; try { reportHTMLPath = await HTML( { @@ -69,7 +81,7 @@ export async function report( finalReportLocation ); - if (reportConfig.reporters.includes("pdf")) { + if (includesPDF) { return await PDF(reportHTMLPath, { title: reportConfig.title, saveOnDisk: savePDFOnDisk, diff --git a/src/constants.js b/src/constants.ts similarity index 100% rename from src/constants.js rename to src/constants.ts diff --git a/src/index.js b/src/index.ts similarity index 100% rename from src/index.js rename to src/index.ts diff --git a/src/localStorage.js b/src/localStorage.js deleted file mode 100644 index 66da95e..0000000 --- a/src/localStorage.js +++ /dev/null @@ -1,8 +0,0 @@ -// Import Node.js Dependencies -import { AsyncLocalStorage } from "node:async_hooks"; - -export const store = new AsyncLocalStorage(); - -export function getConfig() { - return store.getStore(); -} diff --git a/src/localStorage.ts b/src/localStorage.ts new file mode 100644 index 0000000..4f51860 --- /dev/null +++ b/src/localStorage.ts @@ -0,0 +1,16 @@ +// Import Node.js Dependencies +import { AsyncLocalStorage } from "node:async_hooks"; + +// Import Third-party Dependencies +import type { RC } from "@nodesecure/rc"; + +export const store = new AsyncLocalStorage(); + +export function getConfig(): RC { + const runtimeConfig = store.getStore(); + if (runtimeConfig === undefined) { + throw new Error("unable to fetch AsyncLocalStorage runtime config"); + } + + return runtimeConfig; +} diff --git a/src/reporting/html.js b/src/reporting/html.ts similarity index 79% rename from src/reporting/html.js rename to src/reporting/html.ts index f96a751..d6d4680 100644 --- a/src/reporting/html.js +++ b/src/reporting/html.ts @@ -4,13 +4,14 @@ import { readdirSync, promises as fs } from "node:fs"; // Import Third-party Dependencies import esbuild from "esbuild"; +import type { RC } from "@nodesecure/rc"; // Import Internal Dependencies import * as utils from "../utils/index.js"; import * as CONSTANTS from "../constants.js"; import * as localStorage from "../localStorage.js"; - import { HTMLTemplateGenerator } from "./template.js"; +import type { ReportStat } from "../analysis/extractScannerData.js"; // CONSTANTS const kDateFormatter = Intl.DateTimeFormat("en-GB", { @@ -38,31 +39,36 @@ const kStaticESBuildConfig = { sourcemap: true, treeShaking: true, logLevel: "silent" -}; +} as const; const kImagesDir = path.join(CONSTANTS.DIRS.PUBLIC, "img"); -const kAvailableThemes = new Set( +const kAvailableThemes = new Set( readdirSync(CONSTANTS.DIRS.THEMES) .map((file) => path.basename(file, ".css")) ); +export interface HTMLReportData { + pkgStats: ReportStat | null; + repoStats: ReportStat | null; +} + export async function HTML( - data, - reportOptions = null, + data: HTMLReportData, + reportOptions: RC["report"] | null = null, reportOutputLocation = CONSTANTS.DIRS.REPORTS -) { +): Promise { const { pkgStats, repoStats } = data; - const config = reportOptions ?? localStorage.getConfig().report; + const config = reportOptions ?? localStorage.getConfig().report!; const assetsOutputLocation = path.join(reportOutputLocation, "..", "dist"); - const reportTheme = kAvailableThemes.has(config.theme) ? config.theme : "dark"; + const reportTheme = config.theme && kAvailableThemes.has(config.theme) ? config.theme : "dark"; const reportFinalOutputLocation = path.join( reportOutputLocation, utils.cleanReportName(config.title, ".html") ); - const charts = config.charts - .flatMap(({ display, name, help = null }) => (display ? [{ name, help }] : [])); + const charts = (config.charts ?? []) + .flatMap(({ display, name }) => (display ? [{ name }] : [])); const HTMLReport = new HTMLTemplateGenerator( { @@ -92,9 +98,9 @@ export async function HTML( } export async function buildFrontAssets( - outdir, - options = {} -) { + outdir: string, + options: { theme?: string; } = {} +): Promise { const { theme = "light" } = options; await esbuild.build({ diff --git a/src/reporting/index.js b/src/reporting/index.ts similarity index 82% rename from src/reporting/index.js rename to src/reporting/index.ts index f6edc5b..bcdf246 100644 --- a/src/reporting/index.js +++ b/src/reporting/index.ts @@ -6,13 +6,13 @@ import * as utils from "../utils/index.js"; import * as localStorage from "../localStorage.js"; // Import Reporters -import { HTML } from "./html.js"; +import { HTML, type HTMLReportData } from "./html.js"; import { PDF } from "./pdf.js"; export async function proceed( - data, + data: HTMLReportData, verbose = true -) { +): Promise { const reportHTMLPath = await utils.runInSpinner( { title: `[Reporter: ${kleur.yellow().bold("HTML")}]`, @@ -22,7 +22,7 @@ export async function proceed( async() => HTML(data) ); - const { reporters, title } = localStorage.getConfig().report; + const { reporters = [], title } = localStorage.getConfig().report!; if (!reporters.includes("pdf")) { return; } diff --git a/src/reporting/pdf.js b/src/reporting/pdf.ts similarity index 79% rename from src/reporting/pdf.js rename to src/reporting/pdf.ts index b319f76..f91e908 100644 --- a/src/reporting/pdf.js +++ b/src/reporting/pdf.ts @@ -9,10 +9,16 @@ import puppeteer from "puppeteer"; import * as CONSTANTS from "../constants.js"; import * as utils from "../utils/index.js"; +export interface PDFReportOptions { + title: string; + saveOnDisk?: boolean; + reportOutputLocation?: string; +} + export async function PDF( - reportHTMLPath, - options -) { + reportHTMLPath: string, + options: PDFReportOptions +): Promise { const { title, saveOnDisk = true, @@ -41,7 +47,9 @@ export async function PDF( printBackground: true }); - return saveOnDisk ? reportPath : Buffer.from(pdfUint8Array); + return saveOnDisk ? + (reportPath as string) : + Buffer.from(pdfUint8Array); } finally { await page.close(); diff --git a/src/reporting/template.js b/src/reporting/template.ts similarity index 61% rename from src/reporting/template.js rename to src/reporting/template.ts index 1951ac9..3999718 100644 --- a/src/reporting/template.js +++ b/src/reporting/template.ts @@ -4,37 +4,57 @@ import { readFileSync } from "node:fs"; // Import Third-party Dependencies import compile from "zup"; +import type { RC } from "@nodesecure/rc"; // Import Internal Dependencies import * as utils from "../utils/index.js"; import * as CONSTANTS from "../constants.js"; import * as localStorage from "../localStorage.js"; +import type { ReportStat } from "../analysis/extractScannerData.js"; const kHTMLStaticTemplate = readFileSync( path.join(CONSTANTS.DIRS.VIEWS, "template.html"), "utf8" ); +export interface HTMLTemplateGeneratorPayload { + report_theme: string; + report_title: string; + report_logo: string | undefined; + report_date: string; + npm_stats: ReportStat | null; + git_stats: ReportStat | null; + charts: any[]; +} + +export interface HTMLTemplateGenerationRenderOptions { + asset_location?: string; +} + export class HTMLTemplateGenerator { + public payload: HTMLTemplateGeneratorPayload; + public config: RC["report"] | null; + constructor( - payload, - config = null + payload: HTMLTemplateGeneratorPayload, + config: RC["report"] | null = null ) { this.payload = payload; this.config = config; } - render(options = {}) { + render( + options: HTMLTemplateGenerationRenderOptions = {} + ) { const { asset_location = "../dist" } = options; const config = this.config ?? localStorage.getConfig().report; const compiledTemplate = compile(kHTMLStaticTemplate); - /** @type {string} */ const html = compiledTemplate({ ...this.payload, asset_location - }); + }) as string; const charts = [ ...utils.generateChartArray( diff --git a/src/utils/charts.js b/src/utils/charts.js deleted file mode 100644 index 765ce27..0000000 --- a/src/utils/charts.js +++ /dev/null @@ -1,38 +0,0 @@ -// Import Third-party Dependencies -import { taggedString } from "@nodesecure/utils"; - -// CONSTANTS -const kChartTemplate = taggedString`\tcreateChart("${0}", "${4}", { labels: [${1}], interpolate: ${3}, data: [${2}] });`; - -// eslint-disable-next-line max-params -function toChart(baliseName, data, interpolateName, type = "bar") { - const graphLabels = Object - .keys(data) - .map((key) => `"${key}"`) - .join(","); - - return kChartTemplate( - baliseName, - graphLabels, - Object.values(data).join(","), - interpolateName, - type - ); -} - -export function* generateChartArray(pkgStats, repoStats, config) { - const displayableCharts = config.charts.filter((chart) => chart.display); - - if (pkgStats !== null) { - for (const chart of displayableCharts) { - const name = chart.name.toLowerCase(); - yield toChart(`npm_${name}_canvas`, pkgStats[name], chart.interpolation, chart.type); - } - } - if (repoStats !== null) { - for (const chart of displayableCharts) { - const name = chart.name.toLowerCase(); - yield toChart(`git_${name}_canvas`, repoStats[name], chart.interpolation, chart.type); - } - } -} diff --git a/src/utils/charts.ts b/src/utils/charts.ts new file mode 100644 index 0000000..8d78a97 --- /dev/null +++ b/src/utils/charts.ts @@ -0,0 +1,63 @@ +// Import Third-party Dependencies +import { taggedString } from "@nodesecure/utils"; +import type { RC } from "@nodesecure/rc"; + +// Import Internal Dependencies +import type { ReportStat } from "../analysis/extractScannerData.js"; + +// CONSTANTS +const kChartTemplate = taggedString`\tcreateChart("${0}", "${4}", { labels: [${1}], interpolate: ${3}, data: [${2}] });`; + +// eslint-disable-next-line max-params +function toChart( + baliseName: string, + data: object, + interpolateName: string | undefined, + type = "bar" +) { + const graphLabels = Object + .keys(data) + .map((key) => `"${key}"`) + .join(","); + + return kChartTemplate( + baliseName, + graphLabels, + Object.values(data).join(","), + interpolateName!, + type + ); +} + +export function* generateChartArray( + pkgStats: ReportStat | null, + repoStats: ReportStat | null, + config: RC["report"] +) { + const displayableCharts = config?.charts?.filter((chart) => chart.display) ?? []; + + if (pkgStats !== null) { + for (const chart of displayableCharts) { + const name = chart.name.toLowerCase(); + + yield toChart( + `npm_${name}_canvas`, + pkgStats[name], + chart.interpolation, + chart.type + ); + } + } + if (repoStats !== null) { + for (const chart of displayableCharts) { + const name = chart.name.toLowerCase(); + + yield toChart( + `git_${name}_canvas`, + repoStats[name], + chart.interpolation, + chart.type + ); + } + } +} diff --git a/src/utils/cleanReportName.js b/src/utils/cleanReportName.ts similarity index 65% rename from src/utils/cleanReportName.js rename to src/utils/cleanReportName.ts index be5be39..6b5fa98 100644 --- a/src/utils/cleanReportName.js +++ b/src/utils/cleanReportName.ts @@ -4,17 +4,10 @@ import path from "node:path"; // Import Third-party Dependencies import filenamify from "filenamify"; -/** - * @function cleanReportName - * @description clean the report name - * @param {!string} name - * @param {string} [extension=null] - * @returns {string} - */ export function cleanReportName( - name, - extension = null -) { + name: string, + extension: string | null = null +): string { const cleanName = filenamify(name); if (extension === null) { return cleanName; diff --git a/src/utils/cloneGITRepository.js b/src/utils/cloneGITRepository.ts similarity index 57% rename from src/utils/cloneGITRepository.js rename to src/utils/cloneGITRepository.ts index 1671d1f..79eb4d8 100644 --- a/src/utils/cloneGITRepository.js +++ b/src/utils/cloneGITRepository.ts @@ -3,16 +3,11 @@ import { execFile } from "node:child_process"; import { promisify } from "node:util"; const execFilePromise = promisify(execFile); -/** - * @async - * @function cloneGITRepository - * @description clone a given repository from github - * @param {!string} dir - * @param {!string} url - * - * @returns {Promise} - */ -export async function cloneGITRepository(dir, url) { + +export async function cloneGITRepository( + dir: string, + url: string +): Promise { const oauthUrl = url.replace("https://", `https://oauth2:${process.env.GIT_TOKEN}@`); await execFilePromise("git", ["clone", oauthUrl, dir]); diff --git a/src/utils/formatNpmPackages.js b/src/utils/formatNpmPackages.ts similarity index 66% rename from src/utils/formatNpmPackages.js rename to src/utils/formatNpmPackages.ts index 4814f28..dadf28a 100644 --- a/src/utils/formatNpmPackages.js +++ b/src/utils/formatNpmPackages.ts @@ -1,13 +1,7 @@ -/** - * @param {!string} organizationPrefix - * @param {string[]} packages - * - * @returns {string[]} - */ export function formatNpmPackages( - organizationPrefix, - packages -) { + organizationPrefix: string, + packages: string[] +): string[] { if (organizationPrefix === "") { return packages; } diff --git a/src/utils/index.js b/src/utils/index.ts similarity index 100% rename from src/utils/index.js rename to src/utils/index.ts diff --git a/src/utils/runInSpinner.js b/src/utils/runInSpinner.ts similarity index 64% rename from src/utils/runInSpinner.js rename to src/utils/runInSpinner.ts index 7e7b92b..ea0998f 100644 --- a/src/utils/runInSpinner.js +++ b/src/utils/runInSpinner.ts @@ -2,10 +2,18 @@ import { Spinner } from "@topcli/spinner"; import kleur from "kleur"; -export async function runInSpinner( - options, - asyncHandler -) { +export interface RunInSpinnerOptions { + title: string; + start: string; + verbose: boolean; +} + +export type RunInSpinnerHandler = (spinner: Spinner) => Promise; + +export async function runInSpinner( + options: RunInSpinnerOptions, + asyncHandler: RunInSpinnerHandler +): Promise { const { title, verbose = true, start = void 0 } = options; const spinner = new Spinner({ verbose }) @@ -19,7 +27,7 @@ export async function runInSpinner( return response; } - catch (err) { + catch (err: any) { spinner.failed(err.message); throw err; diff --git a/test/api/report.spec.js b/test/api/report.spec.ts similarity index 88% rename from test/api/report.spec.js rename to test/api/report.spec.ts index 086702e..b5d0d5a 100644 --- a/test/api/report.spec.js +++ b/test/api/report.spec.ts @@ -15,38 +15,38 @@ import { report } from "../../src/index.js"; // CONSTANTS const kReportPayload = { title: "test_runner", - theme: "light", + theme: "light" as const, includeTransitiveInternal: false, npm: { - organizationPrefix: null, + organizationPrefix: "@nodesecure", packages: [] }, reporters: [ - "pdf" + "pdf" as const ], charts: [ { - name: "Extensions", + name: "Extensions" as const, display: true, interpolation: "d3.interpolateRainbow", - type: "bar" + type: "bar" as const }, { - name: "Licenses", + name: "Licenses" as const, display: true, interpolation: "d3.interpolateCool", - type: "bar" + type: "bar" as const }, { - name: "Warnings", + name: "Warnings" as const, display: true, - type: "horizontalBar", + type: "horizontalBar" as const, interpolation: "d3.interpolateInferno" }, { - name: "Flags", + name: "Flags" as const, display: true, - type: "horizontalBar", + type: "horizontalBar" as const, interpolation: "d3.interpolateSinebow" } ] @@ -90,7 +90,10 @@ describe("(API) report", { concurrency: 1 }, () => { const generatedPDF = await report( payload.dependencies, - { ...kReportPayload, reporters: ["pdf", "html"] }, + { + ...kReportPayload, + reporters: ["pdf", "html"] + }, { reportOutputLocation, saveHTMLOnDisk: true } ); try { @@ -118,7 +121,10 @@ describe("(API) report", { concurrency: 1 }, () => { const generatedPDFPath = await report( payload.dependencies, - { ...kReportPayload, reporters: ["pdf", "html"] }, + { + ...kReportPayload, + reporters: ["pdf", "html"] + }, { reportOutputLocation, savePDFOnDisk: true } ); try { diff --git a/test/utils/cleanReportName.spec.js b/test/utils/cleanReportName.spec.ts similarity index 100% rename from test/utils/cleanReportName.spec.js rename to test/utils/cleanReportName.spec.ts diff --git a/test/utils/cloneGITRepository.spec.js b/test/utils/cloneGITRepository.spec.ts similarity index 100% rename from test/utils/cloneGITRepository.spec.js rename to test/utils/cloneGITRepository.spec.ts diff --git a/test/utils/formatNpmPackages.spec.js b/test/utils/formatNpmPackages.spec.ts similarity index 100% rename from test/utils/formatNpmPackages.spec.js rename to test/utils/formatNpmPackages.spec.ts diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..ce2f747 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@openally/config.typescript", + "compilerOptions": { + "outDir": "dist" + }, + "include": [ + "src", + "bin", + "package.json" + ], + "exclude": ["node_modules", "dist"] +}