From 1ab31041beac0211c70171969f7e8caf4eb8befa Mon Sep 17 00:00:00 2001 From: PierreDemailly Date: Thu, 23 Jan 2025 21:15:41 +0100 Subject: [PATCH] feat: allow to open the interface with no initial package --- bin/index.js | 1 + docs/cli/open.md | 3 ++- i18n/english.js | 3 ++- i18n/french.js | 3 ++- public/components/navigation/navigation.js | 18 ++++++++++++++ public/components/searchbar/searchbar.css | 4 ++++ public/core/search-nav.js | 4 ++++ public/main.js | 13 ++++++++++ src/commands/http.js | 15 ++++++++++-- src/http-server/cache.js | 28 ++++++++++++++++++---- src/http-server/endpoints/data.js | 7 ++++++ src/http-server/index.js | 13 +++++++--- src/http-server/websocket/search.js | 12 ++++++++++ workspaces/vis-network/src/dataset.js | 7 +++++- workspaces/vis-network/src/utils.js | 4 ++++ 15 files changed, 121 insertions(+), 14 deletions(-) diff --git a/bin/index.js b/bin/index.js index 113c68e7..8d2d8d59 100755 --- a/bin/index.js +++ b/bin/index.js @@ -70,6 +70,7 @@ prog .command("open [json]") .describe(i18n.getTokenSync("cli.commands.open.desc")) .option("-p, --port", i18n.getTokenSync("cli.commands.open.option_port"), process.env.PORT) + .option("-f, --fresh-start", i18n.getTokenSync("cli.commands.open.option_fresh_start"), process.env.PORT) .action(commands.http.start); prog diff --git a/docs/cli/open.md b/docs/cli/open.md index 02b1de07..d2e4ddb1 100644 --- a/docs/cli/open.md +++ b/docs/cli/open.md @@ -10,10 +10,11 @@ $ nsecure open [json] > [!NOTE] > If the `[json]` property is omitted, the command will default to searching for a `nsecure-result.json` file in the current working directory. +> If no `nsecure-result.json` file is found, it will behave as same as with `--fresh-start` option. ## ⚙️ Available Options | Name | Shortcut | Default Value | Description | |---|---|---|---| | `--port` | `-p` | `process.env.PORT` | Specify the port on which the HTTP server should run. | - +| `--fresh-start` | `-f` | `false` | Open the UI with no initial package. Also, the app will use a dedicated cache. | diff --git a/i18n/english.js b/i18n/english.js index 3085c494..8860e591 100644 --- a/i18n/english.js +++ b/i18n/english.js @@ -31,7 +31,8 @@ const cli = { }, open: { desc: "Run an HTTP Server with a given nsecure JSON file", - option_port: "Define the running port" + option_port: "Define the running port", + option_fresh_start: "Launch the server from scratch, ignoring any existing payload file" }, verify: { desc: "Run a complete advanced analysis for a given npm package", diff --git a/i18n/french.js b/i18n/french.js index 7ab82e02..fa5d7312 100644 --- a/i18n/french.js +++ b/i18n/french.js @@ -31,7 +31,8 @@ const cli = { }, open: { desc: "Démarre un serveur HTTP avec un fichier .json nsecure donné", - option_port: "Port à utiliser" + option_port: "Port à utiliser", + option_fresh_start: "Lance le serveur à partir de zéro, en ignorant tout fichier de payload existant" }, verify: { desc: "Démarre une analyse AST avancée pour un package npm donné", diff --git a/public/components/navigation/navigation.js b/public/components/navigation/navigation.js index 9cb3af95..e8d39b83 100644 --- a/public/components/navigation/navigation.js +++ b/public/components/navigation/navigation.js @@ -114,4 +114,22 @@ export class ViewNavigation { this.setNewActiveMenu(selectedNav); } } + + hideMenu(menuName) { + const menu = this.menus.get(menuName); + if (!menu) { + return; + } + + menu.classList.add("hidden"); + } + + showMenu(menuName) { + const menu = this.menus.get(menuName); + if (!menu) { + return; + } + + menu.classList.remove("hidden"); + } } diff --git a/public/components/searchbar/searchbar.css b/public/components/searchbar/searchbar.css index 8f8b0ed6..37b24a56 100644 --- a/public/components/searchbar/searchbar.css +++ b/public/components/searchbar/searchbar.css @@ -213,6 +213,10 @@ div.search-result-pannel .package+.package { box-shadow: 2px 1px 10px #26107f7a; } +#search-nav:has(#searchbar[style*="display: none;"]) { + display: none; +} + #search-nav .search-result-pannel .package { height: 30px; color: rgb(229, 229, 229); diff --git a/public/core/search-nav.js b/public/core/search-nav.js index f8e95adf..589ef1ec 100644 --- a/public/core/search-nav.js +++ b/public/core/search-nav.js @@ -52,6 +52,10 @@ function initPackagesNavigation(data) { classList: ["packages"] }); + if (packages.length === 0) { + return fragment; + } + for (const pkg of packages) { const { name, version, local } = parseNpmSpec(pkg); diff --git a/public/main.js b/public/main.js index 774cda3c..db1928d7 100644 --- a/public/main.js +++ b/public/main.js @@ -101,6 +101,19 @@ async function init(options = {}) { }); await secureDataSet.init(); + if (secureDataSet.data === null) { + window.navigation.hideMenu("network--view"); + window.navigation.hideMenu("home--view"); + window.navigation.setNavByName("search--view"); + + searchview ??= new SearchView(null, null); + + return; + } + + window.navigation.showMenu("network--view"); + window.navigation.showMenu("home--view"); + window.vulnerabilityStrategy = secureDataSet.data.vulnerabilityStrategy; // Initialize vis Network diff --git a/src/commands/http.js b/src/commands/http.js index ab24c37f..66198571 100644 --- a/src/commands/http.js +++ b/src/commands/http.js @@ -1,6 +1,7 @@ // Import Node.js Dependencies import fs from "node:fs"; import path from "node:path"; +import crypto from "node:crypto"; // Import Third-party Dependencies import * as SemVer from "semver"; @@ -9,6 +10,7 @@ import * as i18n from "@nodesecure/i18n"; // Import Internal Dependencies import { buildServer } from "../http-server/index.js"; +import { appCache } from "../http-server/cache.js"; // CONSTANTS const kRequiredScannerRange = ">=5.1.0"; @@ -18,6 +20,7 @@ export async function start( options = {} ) { const port = Number(options.port); + const freshStart = Boolean(options.f); const fileExtension = path.extname(payloadFileBasename); if (fileExtension !== ".json" && fileExtension !== "") { throw new Error("You must provide a JSON file (scanner payload) to open"); @@ -27,10 +30,18 @@ export async function start( process.cwd(), fileExtension === "" ? `${payloadFileBasename}.json` : payloadFileBasename ); - assertScannerVersion(dataFilePath); + const dataFilePathExists = fs.existsSync(dataFilePath); + const runFromPayload = dataFilePathExists && freshStart === false; + if (runFromPayload) { + assertScannerVersion(dataFilePath); + } + else { + appCache.prefix = crypto.randomBytes(4).toString("hex"); + } const httpServer = buildServer(dataFilePath, { - port: Number.isNaN(port) ? 0 : port + port: Number.isNaN(port) ? 0 : port, + runFromPayload }); for (const eventName of ["SIGINT", "SIGTERM"]) { diff --git a/src/http-server/cache.js b/src/http-server/cache.js index 9c4f1627..071cf67a 100644 --- a/src/http-server/cache.js +++ b/src/http-server/cache.js @@ -19,6 +19,9 @@ export const CACHE_PATH = path.join(os.tmpdir(), "nsecure-cli"); export const DEFAULT_PAYLOAD_PATH = path.join(process.cwd(), "nsecure-result.json"); class _AppCache { + prefix = ""; + startFromZero = false; + constructor() { fs.mkdirSync(kPayloadsPath, { recursive: true }); } @@ -58,12 +61,12 @@ class _AppCache { } async updatePayloadsList(payloadsList) { - await cacache.put(CACHE_PATH, kPayloadsCache, JSON.stringify(payloadsList)); + await cacache.put(CACHE_PATH, `${this.prefix}${kPayloadsCache}`, JSON.stringify(payloadsList)); } async payloadsList() { try { - const { data } = await cacache.get(CACHE_PATH, kPayloadsCache); + const { data } = await cacache.get(CACHE_PATH, `${this.prefix}${kPayloadsCache}`); return JSON.parse(data.toString()); } @@ -75,6 +78,21 @@ class _AppCache { } async #initDefaultPayloadsList() { + if (this.startFromZero) { + const payloadsList = { + lru: [], + current: null, + older: [], + lastUsed: {}, + root: null + }; + + logger.info(`[cache|init](startFromZero)`); + await cacache.put(CACHE_PATH, `${this.prefix}${kPayloadsCache}`, JSON.stringify(payloadsList)); + + return; + } + const payload = JSON.parse(fs.readFileSync(DEFAULT_PAYLOAD_PATH, "utf-8")); const version = Object.keys(payload.dependencies[payload.rootDependencyName].versions)[0]; const formatted = `${payload.rootDependencyName}@${version}`; @@ -89,7 +107,7 @@ class _AppCache { }; logger.info(`[cache|init](dep: ${formatted}|version: ${version}|rootDependencyName: ${payload.rootDependencyName})`); - await cacache.put(CACHE_PATH, kPayloadsCache, JSON.stringify(payloadsList)); + await cacache.put(CACHE_PATH, `${this.prefix}${kPayloadsCache}`, JSON.stringify(payloadsList)); this.updatePayload(formatted, payload); } @@ -98,7 +116,7 @@ class _AppCache { try { // prevent re-initialization of the cache - await cacache.get(CACHE_PATH, kPayloadsCache); + await cacache.get(CACHE_PATH, `${this.prefix}${kPayloadsCache}`); return; } @@ -116,7 +134,7 @@ class _AppCache { logger.info(`[cache|init](packagesInFolder: ${packagesInFolder})`); } - await cacache.put(CACHE_PATH, kPayloadsCache, JSON.stringify({ + await cacache.put(CACHE_PATH, `${this.prefix}${kPayloadsCache}`, JSON.stringify({ older: packagesInFolder, current: null, lru: [] diff --git a/src/http-server/endpoints/data.js b/src/http-server/endpoints/data.js index fbc8397a..b40f6ef9 100644 --- a/src/http-server/endpoints/data.js +++ b/src/http-server/endpoints/data.js @@ -13,6 +13,13 @@ import { logger } from "../logger.js"; const kDefaultPayloadPath = path.join(process.cwd(), "nsecure-result.json"); export async function get(_req, res) { + if (appCache.startFromZero) { + logger.info("[data|get](no content)"); + send(res, 204); + + return; + } + try { const { current, lru } = await appCache.payloadsList(); logger.info(`[data|get](current: ${current})`); diff --git a/src/http-server/index.js b/src/http-server/index.js index bab1b82d..3d9d62ce 100644 --- a/src/http-server/index.js +++ b/src/http-server/index.js @@ -22,17 +22,24 @@ import * as report from "./endpoints/report.js"; import * as middleware from "./middleware.js"; import * as wsHandlers from "./websocket/index.js"; import { logger } from "./logger.js"; +import { appCache } from "./cache.js"; export function buildServer(dataFilePath, options = {}) { const httpConfigPort = typeof options.port === "number" ? options.port : 0; const openLink = typeof options.openLink === "boolean" ? options.openLink : true; const enableWS = options.enableWS ?? process.env.NODE_ENV !== "test"; - - fs.accessSync(dataFilePath, fs.constants.R_OK | fs.constants.W_OK); + const runFromPayload = options.runFromPayload ?? true; const httpServer = polka(); - httpServer.use(middleware.buildContextMiddleware(dataFilePath)); + if (runFromPayload) { + fs.accessSync(dataFilePath, fs.constants.R_OK | fs.constants.W_OK); + httpServer.use(middleware.buildContextMiddleware(dataFilePath)); + } + else { + appCache.startFromZero = true; + } + httpServer.use(middleware.addStaticFiles); httpServer.get("/", root.get); diff --git a/src/http-server/websocket/search.js b/src/http-server/websocket/search.js index cff86931..c100712b 100644 --- a/src/http-server/websocket/search.js +++ b/src/http-server/websocket/search.js @@ -22,6 +22,14 @@ export async function search(ws, pkg) { await appCache.updatePayloadsList(updatedList); ws.send(JSON.stringify(cache)); + if (appCache.startFromZero) { + ws.send(JSON.stringify({ + status: "RELOAD", + ...updatedList + })); + appCache.startFromZero = false; + } + return; } @@ -41,6 +49,8 @@ export async function search(ws, pkg) { ...updatedList })); + appCache.startFromZero = false; + return; } @@ -76,6 +86,8 @@ export async function search(ws, pkg) { ...updatedList })); + appCache.startFromZero = false; + logger.info(`[ws|search](data sent to client|cache: updated)`); } } diff --git a/workspaces/vis-network/src/dataset.js b/workspaces/vis-network/src/dataset.js index 82959536..06a8da0e 100644 --- a/workspaces/vis-network/src/dataset.js +++ b/workspaces/vis-network/src/dataset.js @@ -62,9 +62,14 @@ export default class NodeSecureDataSet extends EventTarget { } this.FLAGS = FLAGS; - this.warnings = data.warnings; this.data = data; + if (data === null) { + return; + } + + this.warnings = data.warnings; + const dataEntries = Object.entries(data.dependencies); this.dependenciesCount = dataEntries.length; diff --git a/workspaces/vis-network/src/utils.js b/workspaces/vis-network/src/utils.js index be862926..c8853557 100644 --- a/workspaces/vis-network/src/utils.js +++ b/workspaces/vis-network/src/utils.js @@ -26,6 +26,10 @@ export async function getJSON(path, customHeaders = Object.create(null)) { }; } + if (raw.status === 204) { + return null; + } + return raw.json(); }