diff --git a/src/commands/fix/cmd-fix.mts b/src/commands/fix/cmd-fix.mts index 7fc49e83d..31c431c0d 100644 --- a/src/commands/fix/cmd-fix.mts +++ b/src/commands/fix/cmd-fix.mts @@ -9,6 +9,7 @@ import { handleFix } from './handle-fix.mts' import constants from '../../constants.mts' import { commonFlags } from '../../flags.mts' import { checkCommandInput } from '../../utils/check-input.mts' +import { cmdFlagValueToArray } from '../../utils/cmd.mts' import { getOutputKind } from '../../utils/get-output-kind.mts' import { meowOrExit } from '../../utils/meow-with-subcommands.mts' import { getFlagListOutput } from '../../utils/output-formatting.mts' @@ -38,6 +39,15 @@ const config: CliCommandConfig = { default: false, description: `Shorthand for --autoMerge --test`, }, + ghsa: { + type: 'string', + default: [], + description: `Provide a list of ${terminalLink( + 'GHSA IDs', + 'https://docs.github.com/en/code-security/security-advisories/working-with-global-security-advisories-from-the-github-advisory-database/about-the-github-advisory-database#about-ghsa-ids', + )} to compute fixes for, as either a comma separated value or as multiple flags`, + isMultiple: true, + }, limit: { type: 'number', default: Infinity, @@ -47,9 +57,9 @@ const config: CliCommandConfig = { type: 'string', default: [], description: `Provide a list of ${terminalLink( - 'package URLs', + 'PURLs', 'https://github.com/package-url/purl-spec?tab=readme-ov-file#purl', - )} (PURLs) to fix, as either a comma separated value or as multiple flags,\n instead of querying the Socket API`, + )} to compute fixes for, as either a comma separated value or as multiple flags,\n instead of querying the Socket API`, isMultiple: true, shortFlag: 'p', }, @@ -150,20 +160,18 @@ async function run( test = true } + const ghsas = cmdFlagValueToArray(cli.flags['ghsa']) const limit = (cli.flags['limit'] ? parseInt(String(cli.flags['limit'] || ''), 10) : Infinity) || Infinity - - const purls: string[] = Array.isArray(cli.flags['purl']) - ? cli.flags['purl'].flatMap(p => p.split(/, */)) - : [] - + const purls = cmdFlagValueToArray(cli.flags['purl']) const testScript = String(cli.flags['testScript'] || 'test') await handleFix({ autoMerge, cwd, + ghsas, limit, outputKind, purls, diff --git a/src/commands/fix/handle-fix.mts b/src/commands/fix/handle-fix.mts index aef95c0e8..c55622607 100644 --- a/src/commands/fix/handle-fix.mts +++ b/src/commands/fix/handle-fix.mts @@ -5,6 +5,8 @@ import { outputFixResult } from './output-fix-result.mts' import { pnpmFix } from './pnpm-fix.mts' import { CMD_NAME } from './shared.mts' import constants from '../../constants.mts' +import { cmdFlagValueToArray } from '../../utils/cmd.mts' +import { spawnCoana } from '../../utils/coana.mts' import { detectAndValidatePackageEnvironment } from '../../utils/package-environment.mts' import type { OutputKind } from '../../types.mts' @@ -15,6 +17,7 @@ const { NPM, PNPM } = constants export async function handleFix({ autoMerge, cwd, + ghsas, limit, outputKind, purls, @@ -24,6 +27,7 @@ export async function handleFix({ }: { autoMerge: boolean cwd: string + ghsas: string[] limit: number outputKind: OutputKind purls: string[] @@ -31,21 +35,73 @@ export async function handleFix({ test: boolean testScript: string }) { - const pkgEnvResult = await detectAndValidatePackageEnvironment(cwd, { + let { length: ghsasCount } = ghsas + if (ghsasCount) { + // Lazily access constants.spinner. + const { spinner } = constants + + spinner.start() + + if (ghsasCount === 1 && ghsas[0] === 'auto') { + const autoCResult = await spawnCoana( + ['compute-fixes-and-upgrade-purls', cwd], + { cwd, spinner }, + ) + if (autoCResult.ok) { + console.log(autoCResult.data) + ghsas = cmdFlagValueToArray( + /(?<=Vulnerabilities found: )[^\n]+/.exec( + autoCResult.data as string, + )?.[0], + ) + ghsasCount = ghsas.length + } else { + ghsas = [] + ghsasCount = 0 + } + } + + spinner.stop() + + if (ghsasCount) { + spinner.start() + await outputFixResult( + await spawnCoana( + [ + 'compute-fixes-and-upgrade-purls', + cwd, + '--apply-fixes-to', + ...ghsas, + ], + { cwd, spinner }, + ), + outputKind, + ) + spinner.stop() + return + } + } + + const pkgEnvCResult = await detectAndValidatePackageEnvironment(cwd, { cmdName: CMD_NAME, logger, }) - if (!pkgEnvResult.ok) { - return pkgEnvResult + if (!pkgEnvCResult.ok) { + await outputFixResult(pkgEnvCResult, outputKind) + return } - const pkgEnvDetails = pkgEnvResult.data + const { data: pkgEnvDetails } = pkgEnvCResult if (!pkgEnvDetails) { - return { - ok: false, - message: 'No package found', - cause: `No valid package environment was found in given cwd (${cwd})`, - } + await outputFixResult( + { + ok: false, + message: 'No package found', + cause: `No valid package environment was found in given cwd (${cwd})`, + }, + outputKind, + ) + return } logger.info( @@ -54,27 +110,32 @@ export async function handleFix({ const { agent } = pkgEnvDetails if (agent !== NPM && agent !== PNPM) { - return { - ok: false, - message: 'Not supported', - cause: `${agent} is not supported by this command at the moment.`, - } + await outputFixResult( + { + ok: false, + message: 'Not supported', + cause: `${agent} is not supported by this command at the moment.`, + }, + outputKind, + ) + return } // Lazily access spinner. const { spinner } = constants const fixer = agent === NPM ? npmFix : pnpmFix - const result = await fixer(pkgEnvDetails, { - autoMerge, - cwd, - limit, - purls, - rangeStyle, - spinner, - test, - testScript, - }) - - await outputFixResult(result, outputKind) + await outputFixResult( + await fixer(pkgEnvDetails, { + autoMerge, + cwd, + limit, + purls, + rangeStyle, + spinner, + test, + testScript, + }), + outputKind, + ) } diff --git a/src/commands/scan/scan-reachability.mts b/src/commands/scan/scan-reachability.mts index 42120f472..b771e9b8d 100644 --- a/src/commands/scan/scan-reachability.mts +++ b/src/commands/scan/scan-reachability.mts @@ -1,7 +1,5 @@ -import { spawn } from '@socketsecurity/registry/lib/spawn' - import constants from '../../constants.mts' -import { getDefaultToken } from '../../utils/sdk.mts' +import { spawnCoana } from '../../utils/coana.mts' import type { CResult } from '../../types.mts' @@ -11,34 +9,17 @@ export async function scanReachability( argv: string[] | readonly string[], cwd: string, ): Promise> { - try { - const result = await spawn( - constants.execPath, - [ - // Lazily access constants.nodeNoWarningsFlags. - ...constants.nodeNoWarningsFlags, - // Lazily access constants.coanaBinPath. - constants.coanaBinPath, - 'run', - cwd, - '--output-dir', - cwd, - '--socket-mode', - DOT_SOCKET_DOT_FACTS_JSON, - '--disable-report-submission', - ...argv, - ], - { - cwd, - env: { - ...process.env, - SOCKET_CLI_API_TOKEN: getDefaultToken(), - }, - }, - ) - return { ok: true, data: result.stdout.trim() } - } catch (e) { - const message = (e as any)?.stdout ?? (e as Error)?.message - return { ok: false, data: e, message } - } + return await spawnCoana( + [ + 'run', + cwd, + '--output-dir', + cwd, + '--socket-mode', + DOT_SOCKET_DOT_FACTS_JSON, + '--disable-report-submission', + ...argv, + ], + { cwd }, + ) } diff --git a/src/utils/cmd.mts b/src/utils/cmd.mts index c22b85c5a..37cb3e8a6 100644 --- a/src/utils/cmd.mts +++ b/src/utils/cmd.mts @@ -16,6 +16,16 @@ export function cmdFlagsToString(args: string[]) { return result.join(' ') } +export function cmdFlagValueToArray(flagValue: any): string[] { + if (typeof flagValue === 'string') { + return flagValue.trim().split(/, */) + } + if (Array.isArray(flagValue)) { + return flagValue.flatMap(v => v.split(/, */)) + } + return [] +} + export function cmdPrefixMessage(cmdName: string, text: string): string { const cmdPrefix = cmdName ? `${cmdName}: ` : '' return `${cmdPrefix}${text}` diff --git a/src/utils/coana.mts b/src/utils/coana.mts new file mode 100644 index 000000000..15a7e3cfc --- /dev/null +++ b/src/utils/coana.mts @@ -0,0 +1,45 @@ +import { spawn } from '@socketsecurity/registry/lib/spawn' + +import constants from '../constants.mts' +import { getDefaultToken } from './sdk.mts' + +import type { CResult } from '../types.mts' +import type { + SpawnExtra, + SpawnOptions, +} from '@socketsecurity/registry/lib/spawn' + +export async function spawnCoana( + args: string[] | readonly string[], + options?: SpawnOptions | undefined, + extra?: SpawnExtra | undefined, +): Promise> { + const { env: optionsEnv } = { __proto__: null, ...options } as SpawnOptions + try { + const output = await spawn( + constants.execPath, + [ + // Lazily access constants.nodeNoWarningsFlags. + ...constants.nodeNoWarningsFlags, + // Lazily access constants.coanaBinPath. + constants.coanaBinPath, + ...args, + ], + { + ...options, + env: { + ...process.env, + ...optionsEnv, + SOCKET_CLI_API_BASE_URL: + constants.ENV.SOCKET_CLI_API_BASE_URL || undefined, + SOCKET_CLI_API_TOKEN: getDefaultToken(), + }, + }, + extra, + ) + return { ok: true, data: output.stdout.trim() } + } catch (e) { + const message = (e as any)?.stdout ?? (e as Error)?.message + return { ok: false, data: e, message } + } +}