diff --git a/CHANGELOG.md b/CHANGELOG.md index 55b3e728e..fff3ac0c1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). +## [1.1.133](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.133) - 2026-07-01 + +### Changed +- Quieter `socket manifest gradle`, `sbt`, and `maven`: the build tool's output is now hidden behind a progress spinner and shown only if the build fails. Pass `--verbose` to stream it live. +- `--auto-manifest` now fails fast: if a Gradle, sbt, or Maven build fails, the run stops instead of continuing with an incomplete SBOM. + ## [1.1.132](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.132) - 2026-06-30 ### Changed diff --git a/package.json b/package.json index eb07a3472..3922697d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "socket", - "version": "1.1.132", + "version": "1.1.133", "description": "CLI for Socket.dev", "homepage": "https://github.com/SocketDev/socket-cli", "license": "MIT", diff --git a/src/commands/manifest/generate_auto_manifest.mts b/src/commands/manifest/generate_auto_manifest.mts index 1ad3f4cbd..3e2aa40e4 100644 --- a/src/commands/manifest/generate_auto_manifest.mts +++ b/src/commands/manifest/generate_auto_manifest.mts @@ -13,6 +13,7 @@ import { parseBuildToolOpts } from './parse-build-tool-opts.mts' import { resolveBuildToolBin } from './scripts/build-tool.mts' import { serializeSidecar } from './scripts/sidecar.mts' import { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts' +import { InputError } from '../../utils/errors.mts' import { readOrDefaultSocketJson } from '../../utils/socket-json.mts' import type { GeneratableManifests } from './detect-manifest-actions.mts' @@ -28,12 +29,28 @@ export type GenerateAutoManifestResult = { resolvedPathsSidecar?: ResolvedPathsSidecar | undefined } +// Under --auto-manifest, a manifest generator that failed — raising the exit +// code above the value captured before it ran — aborts the whole run: a partial +// or empty SBOM silently under-reports dependencies. The generator has already +// logged the specifics. A tolerated resolution failure (ignoreUnresolved) warns +// without touching the exit code, so it passes through here and the run +// continues. +function abortManifestRunIfFailed( + ecosystem: string, + beforeExitCode: string | number | undefined, +): void { + if (process.exitCode && process.exitCode !== beforeExitCode) { + throw new InputError( + `Auto-manifest generation failed for the ${ecosystem} project; aborting (see the errors above).`, + ) + } +} + export async function generateAutoManifest({ computeArtifactsSidecar, cwd, detected, outputKind, - reachContinueOnInstallErrors, verbose, }: { // Reachability path: run build tools with files to emit the sidecar. @@ -41,8 +58,6 @@ export async function generateAutoManifest({ detected: GeneratableManifests cwd: string outputKind: OutputKind - // Reachability install-error gate: tolerate a blocking resolution failure. - reachContinueOnInstallErrors?: boolean | undefined verbose: boolean }): Promise { const sockJson = readOrDefaultSocketJson(cwd) @@ -52,11 +67,6 @@ export async function generateAutoManifest({ const sidecarAcc: SidecarAccumulator | undefined = computeArtifactsSidecar ? new Map() : undefined - // Reachability: the install-error gate decides abort; manifest path: socket.json. - const resolveIgnoreUnresolved = (configured: boolean): boolean => - computeArtifactsSidecar - ? configured || Boolean(reachContinueOnInstallErrors) - : configured if (verbose) { logger.info(`Using this ${SOCKET_JSON} for defaults:`, sockJson) @@ -77,22 +87,26 @@ export async function generateAutoManifest({ // `defaults.manifest.sbt.facts: false` in socket.json. if (sockJson.defaults?.manifest?.sbt?.facts !== false) { logger.log('Detected a Scala sbt build, generating Socket facts...') + const beforeExitCode = process.exitCode await convertSbtToFacts({ ...sbtArgs, excludeConfigs: sockJson.defaults?.manifest?.sbt?.excludeConfigs ?? '', - ignoreUnresolved: resolveIgnoreUnresolved( - Boolean(sockJson.defaults?.manifest?.sbt?.ignoreUnresolved), + ignoreUnresolved: Boolean( + sockJson.defaults?.manifest?.sbt?.ignoreUnresolved, ), includeConfigs: sockJson.defaults?.manifest?.sbt?.includeConfigs ?? '', sidecarAcc, withFiles: computeArtifactsSidecar, }) + abortManifestRunIfFailed('sbt', beforeExitCode) } else { logger.log('Detected a Scala sbt build, generating pom files with sbt...') + const beforeExitCode = process.exitCode await convertSbtToMaven({ ...sbtArgs, out: sockJson.defaults?.manifest?.sbt?.outfile ?? './pom.xml', }) + abortManifestRunIfFailed('sbt', beforeExitCode) } } @@ -114,28 +128,33 @@ export async function generateAutoManifest({ logger.log( 'Detected a gradle build (Gradle, Kotlin, Scala), generating Socket facts...', ) + const beforeExitCode = process.exitCode await convertGradleToFacts({ ...gradleArgs, excludeConfigs: sockJson.defaults?.manifest?.gradle?.excludeConfigs ?? '', - ignoreUnresolved: resolveIgnoreUnresolved( - Boolean(sockJson.defaults?.manifest?.gradle?.ignoreUnresolved), + ignoreUnresolved: Boolean( + sockJson.defaults?.manifest?.gradle?.ignoreUnresolved, ), includeConfigs: sockJson.defaults?.manifest?.gradle?.includeConfigs ?? '', sidecarAcc, withFiles: computeArtifactsSidecar, }) + abortManifestRunIfFailed('gradle', beforeExitCode) } else { logger.log( 'Detected a gradle build (Gradle, Kotlin, Scala), running default gradle generator...', ) + const beforeExitCode = process.exitCode await convertGradleToMaven(gradleArgs) + abortManifestRunIfFailed('gradle', beforeExitCode) } } if (!sockJson?.defaults?.manifest?.maven?.disabled && detected.maven) { logger.log('Detected a Maven pom.xml build, generating Socket facts...') + const beforeExitCode = process.exitCode await convertMavenToFacts({ // Configured bin wins; else prefer ./mvnw, else mvn on PATH. bin: @@ -143,8 +162,8 @@ export async function generateAutoManifest({ resolveBuildToolBin('maven', cwd), cwd, excludeConfigs: sockJson.defaults?.manifest?.maven?.excludeConfigs ?? '', - ignoreUnresolved: resolveIgnoreUnresolved( - Boolean(sockJson.defaults?.manifest?.maven?.ignoreUnresolved), + ignoreUnresolved: Boolean( + sockJson.defaults?.manifest?.maven?.ignoreUnresolved, ), includeConfigs: sockJson.defaults?.manifest?.maven?.includeConfigs ?? '', mavenOpts: parseBuildToolOpts( @@ -154,6 +173,7 @@ export async function generateAutoManifest({ verbose: Boolean(sockJson.defaults?.manifest?.maven?.verbose), withFiles: computeArtifactsSidecar, }) + abortManifestRunIfFailed('maven', beforeExitCode) } if (!sockJson?.defaults?.manifest?.conda?.disabled && detected.conda) { diff --git a/src/commands/manifest/run-manifest-facts.mts b/src/commands/manifest/run-manifest-facts.mts index 0a3e8039c..52b631b59 100644 --- a/src/commands/manifest/run-manifest-facts.mts +++ b/src/commands/manifest/run-manifest-facts.mts @@ -7,16 +7,29 @@ import { renderResolutionErrorReport } from './scripts/resolution-report-render. import { runManifestScript } from './scripts/run.mts' import { accumulateSidecar } from './scripts/sidecar.mts' import constants from '../../constants.mts' -import { InputError } from '../../utils/errors.mts' import type { BuildTool } from './scripts/build-tool.mts' +import type { ManifestRunResult } from './scripts/run.mts' import type { SidecarAccumulator } from './scripts/sidecar.mts' +const MAX_FAILURE_OUTPUT_LINES = 40 + +// Last N non-empty lines of the captured build output, for diagnosing a crash +// without forcing a --verbose rebuild. +function tailBuildOutput(stdout: string, stderr: string): string { + const combined = [stdout, stderr] + .map(s => s.trimEnd()) + .filter(Boolean) + .join('\n') + return combined.split('\n').slice(-MAX_FAILURE_OUTPUT_LINES).join('\n') +} + // Runs the bundled build-tool resolution script for a JVM project and writes // `.socket.facts.json`. `withFiles` (reachability only) additionally folds -// resolved artifact paths into `sidecarAcc`. A blocking resolution failure — or -// a build that crashes without emitting any facts — throws unless -// `ignoreUnresolved`. +// resolved artifact paths into `sidecarAcc`. A blocking resolution failure sets +// a non-zero exit code and returns (matching the `--pom` generator) unless +// `ignoreUnresolved`; a crashed build — a process failure, not an unresolved +// dependency — always fails. export async function runManifestFacts({ bin, buildOpts, @@ -46,17 +59,53 @@ export async function runManifestFacts({ `Generating Socket facts for the ${ecosystem} project at \`${cwd}\` ...`, ) - const { artifactPaths, code, facts, report } = await runManifestScript( - ecosystem, - { - bin: bin || undefined, - excludeConfigs: excludeConfigs || undefined, - includeConfigs: includeConfigs || undefined, - projectDir: cwd, - toolOpts: buildOpts, - withFiles, - }, - ) + const scriptOpts = { + bin: bin || undefined, + excludeConfigs: excludeConfigs || undefined, + includeConfigs: includeConfigs || undefined, + projectDir: cwd, + // Stream the build tool's output only when asked; otherwise capture it and + // show a spinner, surfacing the output only if the build crashes. + stdio: verbose ? ('inherit' as const) : ('pipe' as const), + toolOpts: buildOpts, + withFiles, + } + const { spinner } = constants + let result: ManifestRunResult + try { + if (verbose) { + logger.info( + `(Running ${ecosystem} with output streaming; this can take a while.)`, + ) + result = await runManifestScript(ecosystem, scriptOpts) + } else { + logger.info( + `(No live output; pass --verbose to stream the ${ecosystem} build output.)`, + ) + spinner.start(`Resolving ${ecosystem} dependencies ...`) + result = await runManifestScript(ecosystem, scriptOpts) + if (result.code === 0) { + spinner.successAndStop(`Resolved ${ecosystem} dependencies.`) + } else { + spinner.failAndStop( + `${ecosystem} build exited with code ${result.code}.`, + ) + } + } + } catch (e) { + // Only a spawn-level failure (e.g. the build tool missing from PATH) reaches + // here; runNeverThrow returns non-zero build exits rather than throwing. + if (!verbose) { + spinner.failAndStop(`Failed to run ${ecosystem}.`) + } + process.exitCode = 1 + logger.fail( + `Could not run the ${ecosystem} build tool` + + (verbose ? `: ${e}` : ' (run with --verbose for details).'), + ) + return + } + const { artifactPaths, code, facts, report, stderr, stdout } = result const rendered = renderResolutionErrorReport( report.failures, @@ -69,10 +118,12 @@ export async function runManifestFacts({ if (ignoreUnresolved) { logger.warn(rendered.summary) } else { + process.exitCode = 1 + logger.fail(rendered.summary) if (verbose && rendered.details) { logger.log(rendered.details) } - throw new InputError(rendered.summary) + return } } if (rendered.nonBlockingNotice) { @@ -94,11 +145,22 @@ export async function runManifestFacts({ !facts.projects?.length && !report.failures.length ) { - const message = `The ${ecosystem} build failed (exit code ${code}) before producing any Socket facts. Re-run with --verbose for the build tool's output.` - if (!ignoreUnresolved) { - throw new InputError(message) + if (!verbose) { + const tail = tailBuildOutput(stdout, stderr) + if (tail) { + logger.group('Build output:') + logger.error(tail) + logger.groupEnd() + } } - logger.warn(message) + // A crashed build is a process failure (missing JDK/build tool, unparseable + // project, OOM, plugin error), not an unresolved dependency, so it fails + // regardless of `ignoreUnresolved` — that flag only tolerates dependencies a + // successful run couldn't resolve. + process.exitCode = 1 + logger.fail( + `The ${ecosystem} build failed (exit code ${code}) before producing any Socket facts.`, + ) return } diff --git a/src/commands/manifest/scripts/run.mts b/src/commands/manifest/scripts/run.mts index 68343d148..13544c602 100644 --- a/src/commands/manifest/scripts/run.mts +++ b/src/commands/manifest/scripts/run.mts @@ -2,7 +2,7 @@ import { existsSync, promises as fs } from 'node:fs' import { tmpdir } from 'node:os' import path from 'node:path' -import { isSpawnError, spawn } from '@socketsecurity/registry/lib/spawn' +import { spawn } from '@socketsecurity/registry/lib/spawn' import { assembleFacts } from './assemble.mts' import { resolveBuildToolBin } from './build-tool.mts' @@ -34,6 +34,9 @@ export type ManifestRunResult = { facts: SocketFactsSbom report: ResolutionReport artifactPaths: ResolvedArtifactPaths + // Captured build-tool output (empty when stdio is 'inherit'). + stderr: string + stdout: string } type RunOutput = { code: number; stdout: string; stderr: string } @@ -67,11 +70,23 @@ async function runNeverThrow( stderr: typeof result.stderr === 'string' ? result.stderr : '', } } catch (e) { - if (isSpawnError(e)) { + // A build tool that exits non-zero rejects with the spawn-result shape: a + // numeric exit `code` plus captured stdout/stderr. Return it so the caller + // can assemble failure records / surface the output. Anything else — e.g. a + // missing executable, whose `code` is a string like 'ENOENT' — propagates. + // This mirrors how utils/dlx.mts classifies spawn failures (a numeric `code` + // is a real process exit); the registry's isSpawnError is avoided here + // because it is currently broken and never matches. + if ( + e !== null && + typeof e === 'object' && + typeof (e as { code?: unknown }).code === 'number' + ) { + const err = e as { code: number; stdout?: unknown; stderr?: unknown } return { - code: e.code, - stdout: typeof e.stdout === 'string' ? e.stdout : '', - stderr: typeof e.stderr === 'string' ? e.stderr : '', + code: err.code, + stdout: typeof err.stdout === 'string' ? err.stdout : '', + stderr: typeof err.stderr === 'string' ? err.stderr : '', } } throw e @@ -101,14 +116,21 @@ async function writeSbtPlugin(globalBase: string): Promise { } async function assembleFromRecords( - code: number, + out: RunOutput, recordsFile: string, ): Promise { const text = existsSync(recordsFile) ? await fs.readFile(recordsFile, 'utf8') : '' const { artifactPaths, facts, report } = assembleFacts(parseRecords(text)) - return { code, facts, report, artifactPaths } + return { + code: out.code, + facts, + report, + artifactPaths, + stderr: out.stderr, + stdout: out.stdout, + } } // Missing only in an unbuilt local checkout. Fail loudly: without the extension, @@ -179,7 +201,7 @@ async function runGradle( '--console=plain', ] const out = await runNeverThrow(bin, args, opts) - return await assembleFromRecords(out.code, recordsFile) + return await assembleFromRecords(out, recordsFile) }) } @@ -211,7 +233,7 @@ async function runSbt(opts: ManifestScriptOptions): Promise { FACTS_TASK, ] const out = await runNeverThrow(bin, args, opts) - return await assembleFromRecords(out.code, recordsFile) + return await assembleFromRecords(out, recordsFile) }) } @@ -241,6 +263,6 @@ async function runMaven( 'validate', ] const out = await runNeverThrow(bin, args, opts) - return await assembleFromRecords(out.code, recordsFile) + return await assembleFromRecords(out, recordsFile) }) } diff --git a/src/commands/scan/handle-create-new-scan.mts b/src/commands/scan/handle-create-new-scan.mts index 1e2fb100d..bd9bb0faf 100644 --- a/src/commands/scan/handle-create-new-scan.mts +++ b/src/commands/scan/handle-create-new-scan.mts @@ -143,7 +143,6 @@ export async function handleCreateNewScan({ cwd, detected, outputKind, - reachContinueOnInstallErrors: reach.reachContinueOnInstallErrors, verbose: false, }) resolvedPathsSidecar = autoManifestResult.resolvedPathsSidecar