diff --git a/package-lock.json b/package-lock.json index cb0c2fcc..031d1620 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,6 +15,7 @@ "@oclif/plugin-help": "^6.2.29", "@oclif/plugin-update": "^4.6.45", "graphql": "^16.11.0", + "ora": "^8.2.0", "packageurl-js": "^2.0.1", "terminal-link": "^4.0.0", "update-notifier": "^7.3.1" @@ -8710,6 +8711,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-npm": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.0.0.tgz", @@ -8779,6 +8792,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-wsl": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", @@ -9136,6 +9161,34 @@ "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", "license": "MIT" }, + "node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -9302,6 +9355,18 @@ "node": ">=6" } }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mimic-response": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", @@ -10110,6 +10175,125 @@ "tslib": "^2.3.0" } }, + "node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ora/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "license": "MIT", + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "license": "MIT" + }, + "node_modules/ora/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "license": "MIT", + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "license": "MIT", + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/os-locale": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", @@ -11812,6 +11996,18 @@ "node": ">= 0.6" } }, + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/string_decoder": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", diff --git a/package.json b/package.json index 23e7c32c..fd3b3864 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "@oclif/plugin-help": "^6.2.29", "@oclif/plugin-update": "^4.6.45", "graphql": "^16.11.0", + "ora": "^8.2.0", "packageurl-js": "^2.0.1", "terminal-link": "^4.0.0", "update-notifier": "^7.3.1" diff --git a/src/commands/scan/eol.ts b/src/commands/scan/eol.ts index 7b50c80a..8b05d822 100644 --- a/src/commands/scan/eol.ts +++ b/src/commands/scan/eol.ts @@ -1,6 +1,7 @@ import fs from 'node:fs'; import path from 'node:path'; import { Command, Flags, ux } from '@oclif/core'; +import ora from 'ora'; import terminalLink from 'terminal-link'; import { batchSubmitPurls } from '../../api/nes/nes.client.ts'; import type { ScanResult } from '../../api/types/hd-cli.types.js'; @@ -70,9 +71,8 @@ export default class ScanEol extends Command { private async getScan(flags: Record, config: Command['config']): Promise { if (flags.purls) { - ux.action.start(`Scanning purls from ${flags.purls}`); const purls = this.getPurlsFromFile(flags.purls); - return batchSubmitPurls(purls); + return this.scanPurls(purls); } const sbom = await ScanSbom.loadSbom(flags, config); @@ -80,10 +80,14 @@ export default class ScanEol extends Command { } private getPurlsFromFile(filePath: string): string[] { + const spinner = ora().start(`Loading purls from \`${filePath}\``); try { const purlsFileString = fs.readFileSync(filePath, 'utf8'); - return parsePurlsFile(purlsFileString); + const purls = parsePurlsFile(purlsFileString); + spinner.succeed(`Loaded purls from \`${filePath}\``); + return purls; } catch (error) { + spinner.fail(`Failed to read purls from \`${filePath}\``); this.error(`Failed to read purls file. ${getErrorMessage(error)}`); } } @@ -100,21 +104,24 @@ export default class ScanEol extends Command { } private async scanSbom(sbom: Sbom): Promise { - let scan: ScanResult; - let purls: string[]; - try { - purls = await extractPurls(sbom); + const purls = await extractPurls(sbom); + return this.scanPurls(purls); } catch (error) { this.error(`Failed to extract purls from sbom. ${getErrorMessage(error)}`); } + } + + private async scanPurls(purls: string[]): Promise { + const spinner = ora().start('Scanning for EOL packages'); try { - scan = await batchSubmitPurls(purls); + const scan = await batchSubmitPurls(purls); + spinner.succeed('Scan completed'); + return scan; } catch (error) { - this.error(`Failed to submit scan to NES from sbom. ${getErrorMessage(error)}`); + spinner.fail('Scanning failed'); + this.error(`Failed to submit scan to NES. ${getErrorMessage(error)}`); } - - return scan; } private async saveReport(components: InsightsEolScanComponent[], createdOn?: string): Promise { diff --git a/src/commands/scan/sbom.ts b/src/commands/scan/sbom.ts index b11d4a9e..fc9bae1c 100644 --- a/src/commands/scan/sbom.ts +++ b/src/commands/scan/sbom.ts @@ -1,7 +1,8 @@ import { spawn } from 'node:child_process'; import fs from 'node:fs'; import { join, resolve } from 'node:path'; -import { Command, Flags, ux } from '@oclif/core'; +import { Command, Flags } from '@oclif/core'; +import ora from 'ora'; import { filenamePrefix } from '../../config/constants.ts'; import type { Sbom } from '../../service/eol/cdx.svc.ts'; import { createSbom, validateIsCycloneDxSbom } from '../../service/eol/eol.svc.ts'; @@ -73,22 +74,31 @@ export default class ScanSbom extends Command { } let sbom: Sbom; const path = dir || process.cwd(); + + const spinner = ora(); + if (!background) { + spinner.start(flags.file ? 'Loading SBOM file' : 'Generating SBOM'); + } + if (file) { sbom = this._getSbomFromFile(file); - ux.action.stop(); } else if (background) { this._getSbomInBackground(path); this.log(`The scan is running in the background. The file will be saved at ${path}/${filenamePrefix}.sbom.json`); - ux.action.stop(); return; } else { sbom = await this._getSbomFromScan(path); - ux.action.stop(); if (save) { this._saveSbom(path, sbom); } } + if (sbom) { + spinner.succeed(flags.file ? 'Loaded SBOM file' : 'Generated SBOM'); + } else { + spinner.fail(flags.file ? 'Failed to load SBOM file' : 'Failed to generate SBOM'); + } + if (!save) { this.log(JSON.stringify(sbom, null, 2)); } @@ -107,8 +117,6 @@ export default class ScanSbom extends Command { this.error(`Path is not a directory: ${dir}`); } - ux.action.start(`Scanning ${dir}`); - const options = this.getScanOptions(); const sbom = await createSbom(dir, options); if (!sbom) { @@ -149,8 +157,6 @@ export default class ScanSbom extends Command { this.error(`SBOM file not found: ${file}`); } - ux.action.start(`Loading sbom from ${file}`); - const fileContent = fs.readFileSync(file, { encoding: 'utf8', flag: 'r',