From 916595508c4d879b1e44de7462cac21e6403e84b Mon Sep 17 00:00:00 2001 From: Jeppe Fredsgaard Blaabjerg Date: Mon, 17 Aug 2026 15:53:06 +0200 Subject: [PATCH] make --dynamic-sbom-inference a standalone flag The flag lived in reachabilityFlags, so when the --reach guard switched from a hand-maintained list to a derivation over every boolean in that object, `scan create . --dynamic-sbom-inference` started failing with "Reachability analysis flags require --reach to be enabled". It is not a --reach-* modifier; move it to generalFlags so the guard no longer sees it and the help text stops filing it under Reachability Options. It also implied --auto-manifest to get its per-build-root facts generated, and the handler suppressed only the JVM entries of the detection result before calling generateAutoManifest. Conda and Bazel survived that, so asking for JVM dynamic SBOM inference generated their manifests too. Run the recursive JVM generation on its own instead, and reach generateAutoManifest only when --auto-manifest was genuinely requested, which also makes the two flags additive rather than one silently forcing the other. Drops the side effect where the forced auto-manifest overrode an explicit `autoManifest: false` in socket.json. --- CHANGELOG.md | 6 ++ src/commands/scan/cmd-scan-create.mts | 18 +++-- src/commands/scan/cmd-scan-create.test.mts | 32 ++++++++- src/commands/scan/cmd-scan-reach.mts | 23 ++---- src/commands/scan/handle-create-new-scan.mts | 70 +++++++++++-------- .../scan/handle-create-new-scan.test.mts | 42 +++++++++-- src/commands/scan/reachability-flags.mts | 6 -- 7 files changed, 129 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 02614c3ad7..de7a4b589f 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/). +## [Unreleased] + +### Fixed +- `socket scan create --dynamic-sbom-inference` works without `--reach` again, so a single command produces a per-build-root Socket facts SBOM for Gradle, sbt, and Maven projects. It is a standalone flag now, no longer a reachability modifier. +- `--dynamic-sbom-inference` no longer generates Conda or Bazel manifests as a side effect. It touches Gradle, sbt, and Maven only; pass `--auto-manifest` alongside it if you want the other ecosystems too. + ## [1.1.158](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.158) - 2026-08-17 ### Changed diff --git a/src/commands/scan/cmd-scan-create.mts b/src/commands/scan/cmd-scan-create.mts index 41564b50c5..80e7716070 100644 --- a/src/commands/scan/cmd-scan-create.mts +++ b/src/commands/scan/cmd-scan-create.mts @@ -92,6 +92,12 @@ const generalFlags: MeowFlags = { description: 'Set the default branch of the repository to the branch of this full-scan. Should only need to be done once, for example for the "main" or "master" branch.', }, + dynamicSbomInference: { + type: 'boolean', + default: false, + description: + 'For Gradle, sbt, and Maven: generate a Socket facts SBOM (produced directly by each package manager) per independent build root, instead of one synthetic root. Combine with --reach to split the reachability analysis per project/module.', + }, interactive: { type: 'boolean', default: true, @@ -355,11 +361,6 @@ async function run( autoManifest = false } } - // --dynamic-sbom-inference requires auto-manifest to generate the - // per-workspace facts it feeds to Coana. - if (dynamicSbomInference) { - autoManifest = true - } if (!branchName) { if (sockJson.defaults?.scan?.create?.branch) { branchName = sockJson.defaults.scan.create.branch @@ -455,7 +456,12 @@ async function run( const hasFactsFile = existsSync( path.join(cwd, constants.DOT_SOCKET_DOT_FACTS_JSON), ) - if (detected.count > 0 && !autoManifest && !hasFactsFile) { + if ( + detected.count > 0 && + !autoManifest && + !dynamicSbomInference && + !hasFactsFile + ) { logger.info( `Detected ${detected.count} manifest targets we could try to generate. Please set the --auto-manifest flag if you want to include languages covered by \`socket manifest auto\` in the Scan.`, ) diff --git a/src/commands/scan/cmd-scan-create.test.mts b/src/commands/scan/cmd-scan-create.test.mts index ddaaa47cde..03d456c03a 100644 --- a/src/commands/scan/cmd-scan-create.test.mts +++ b/src/commands/scan/cmd-scan-create.test.mts @@ -40,6 +40,7 @@ describe('socket scan create', async () => { --committers Committers --cwd working directory, defaults to process.cwd() --default-branch Set the default branch of the repository to the branch of this full-scan. Should only need to be done once, for example for the "main" or "master" branch. + --dynamic-sbom-inference For Gradle, sbt, and Maven: generate a Socket facts SBOM (produced directly by each package manager) per independent build root, instead of one synthetic root. Combine with --reach to split the reachability analysis per project/module. --exclude-paths List of glob patterns to exclude from the scan, including SCA/SBOM manifest discovery and (when --reach is enabled) full application reachability analysis. Patterns are anchored micromatch globs matched relative to the Socket scan root, which is the command working directory (\`--cwd\` if set), not the reachability target: \`tests\` matches only \`/tests\`; use \`**/tests\` to match at any depth. Negation patterns (\`!path\`) are not supported. Accepts a comma-separated value or multiple flags. --interactive Allow for interactive elements, asking for input. Use --no-interactive to prevent any input questions, defaulting them to cancel/no. --json Output as JSON @@ -56,7 +57,6 @@ describe('socket scan create', async () => { --workspace The workspace in the Socket Organization that the repository is in to associate with the full scan. Reachability Options (when --reach is used) - --dynamic-sbom-inference For Gradle, sbt, and Maven: splits reachability analysis per project/module using a Socket facts SBOM (generated directly by each package manager) per build root, instead of one synthetic root. Reachability analysis only; implies --auto-manifest. --reach-analysis-memory-limit The maximum memory for the reachability analysis as a whole number optionally followed by MB or GB (e.g. 512MB, 8GB). The default is 8GB. --reach-analysis-timeout Set the timeout for the reachability analysis as a whole number optionally followed by s, m or h (e.g. 90s, 10m, 1h). Defaults to 10m. Split analysis runs may cause the total scan time to exceed this timeout significantly. --reach-concurrency Set the maximum number of concurrent reachability analysis runs. It is recommended to choose a concurrency level that ensures each analysis run has at least the --reach-analysis-memory-limit amount of memory available. @@ -276,6 +276,36 @@ describe('socket scan create', async () => { }, ) + cmdit( + [ + 'scan', + 'create', + FLAG_ORG, + 'fakeOrg', + 'target', + FLAG_DRY_RUN, + '--repo', + 'xyz', + '--branch', + 'abc', + '--dynamic-sbom-inference', + FLAG_CONFIG, + '{"apiToken":"fakeToken"}', + ], + 'should succeed when --dynamic-sbom-inference is used without --reach', + async cmd => { + const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd) + expect(stdout).toMatchInlineSnapshot(`"[DryRun]: Bailing now"`) + expect(stdout + stderr).not.toContain( + 'Reachability analysis flags require --reach to be enabled', + ) + expect( + code, + 'should exit with code 0 since it is not a reachability modifier', + ).toBe(0) + }, + ) + cmdit( [ 'scan', diff --git a/src/commands/scan/cmd-scan-reach.mts b/src/commands/scan/cmd-scan-reach.mts index 70c2808426..94097c2bcf 100644 --- a/src/commands/scan/cmd-scan-reach.mts +++ b/src/commands/scan/cmd-scan-reach.mts @@ -33,21 +33,6 @@ const description = 'Compute full application reachability' const hidden = true -// dynamicSbomInference relies on --auto-manifest generating per-workspace -// Socket facts first, which this command never runs (see the hardcoded -// `false` passed to handleScanReach below) - hidden here even though it's -// otherwise public on `scan create`, since advertising a flag this command -// silently ignores would be misleading. -const reachabilityFlagsForReach: MeowFlags = { - ...reachabilityFlags, - dynamicSbomInference: { - type: 'boolean', - default: false, - hidden: true, - description: reachabilityFlags['dynamicSbomInference']!.description, - }, -} - const generalFlags: MeowFlags = { ...commonFlags, ...outputFlags, @@ -89,7 +74,7 @@ async function run( flags: { ...generalFlags, ...excludePathsFlag, - ...reachabilityFlagsForReach, + ...reachabilityFlags, }, help: command => ` @@ -103,7 +88,7 @@ async function run( ${getFlagListOutput(generalFlags)} Reachability Options - ${getFlagListOutput({ ...excludePathsFlag, ...reachabilityFlagsForReach })} + ${getFlagListOutput({ ...excludePathsFlag, ...reachabilityFlags })} Runs the Socket reachability analysis without creating a scan in Socket. The output is written to .socket.facts.json in the current working directory @@ -282,8 +267,8 @@ async function run( outputKind, outputPath: outputPath || '', reachabilityOptions: { - // Not exposed here: it relies on --auto-manifest generating per-workspace - // Socket facts first, which `socket scan reach` never runs. + // Not exposed here: it relies on the per-build-root Socket facts that + // only `socket scan create` generates. dynamicSbomInference: false, excludePaths, reachAnalysisMemoryLimit, diff --git a/src/commands/scan/handle-create-new-scan.mts b/src/commands/scan/handle-create-new-scan.mts index 1b2109d68f..d6d1ff9579 100644 --- a/src/commands/scan/handle-create-new-scan.mts +++ b/src/commands/scan/handle-create-new-scan.mts @@ -157,21 +157,15 @@ export async function handleCreateNewScan({ // paths. Always allocated (even when unused) to keep this uniform rather // than conditional on autoManifest/reach. await withTmpDir('socket-auto-manifest-', async manifestTmpDir => { - if (autoManifest) { + if (autoManifest || reach.dynamicSbomInference) { logger.info('Auto-generating manifest files ...') - debugFn('notice', 'Auto-manifest mode enabled') - const sockJson = readOrDefaultSocketJson(cwd) - const detected = await detectManifestActions(sockJson, cwd) - debugDir('inspect', { detected }) + debugFn('notice', 'Manifest auto-generation enabled') if (reach.dynamicSbomInference) { // Recursively discover and generate Socket facts for every // independent gradle/sbt/maven build root instead of only the one at - // cwd; generateAutoManifest below is left to handle conda/bazel only. - detected.gradle = false - detected.sbt = false - detected.maven = false - + // cwd. This runs on its own, so nothing outside those three + // ecosystems is generated unless --auto-manifest also asked for it. const sidecarAcc: SidecarAccumulator | undefined = reach.runReachabilityAnalysis ? new Map() : undefined const outcomes = await generateRecursiveManifests({ @@ -218,27 +212,41 @@ export async function handleCreateNewScan({ } } - const autoManifestResult = await generateAutoManifest({ - computeArtifactsSidecar: reach.runReachabilityAnalysis, - cwd, - detected, - excludePaths: reach.excludePaths, - outputKind, - tmpDir: manifestTmpDir, - verbose: false, - }) - if (autoManifestResult.resolvedPathsSidecar) { - resolvedPathsSidecar = resolvedPathsSidecar - ? mergeResolvedPathsSidecars( - resolvedPathsSidecar, - autoManifestResult.resolvedPathsSidecar, - ) - : autoManifestResult.resolvedPathsSidecar - } - if (autoManifestResult.generatedFiles.length) { - scanTargets = Array.from( - new Set([...scanTargets, ...autoManifestResult.generatedFiles]), - ) + if (autoManifest) { + const sockJson = readOrDefaultSocketJson(cwd) + const detected = await detectManifestActions(sockJson, cwd) + debugDir('inspect', { detected }) + + if (reach.dynamicSbomInference) { + // Already generated recursively above; resolving cwd's own build + // root a second time would race on the same .socket.facts.json. + detected.gradle = false + detected.sbt = false + detected.maven = false + } + + const autoManifestResult = await generateAutoManifest({ + computeArtifactsSidecar: reach.runReachabilityAnalysis, + cwd, + detected, + excludePaths: reach.excludePaths, + outputKind, + tmpDir: manifestTmpDir, + verbose: false, + }) + if (autoManifestResult.resolvedPathsSidecar) { + resolvedPathsSidecar = resolvedPathsSidecar + ? mergeResolvedPathsSidecars( + resolvedPathsSidecar, + autoManifestResult.resolvedPathsSidecar, + ) + : autoManifestResult.resolvedPathsSidecar + } + if (autoManifestResult.generatedFiles.length) { + scanTargets = Array.from( + new Set([...scanTargets, ...autoManifestResult.generatedFiles]), + ) + } } logger.info('Auto-generation finished. Proceeding with Scan creation.') } diff --git a/src/commands/scan/handle-create-new-scan.test.mts b/src/commands/scan/handle-create-new-scan.test.mts index fdd5fe5f90..fb47a65beb 100644 --- a/src/commands/scan/handle-create-new-scan.test.mts +++ b/src/commands/scan/handle-create-new-scan.test.mts @@ -178,7 +178,39 @@ describe('handleCreateNewScan excludePaths', () => { expect(mockFetchCreateOrgFullScan).toHaveBeenCalled() }) - it('drives JVM facts generation through generateRecursiveManifests under --dynamic-sbom-inference, merging generated facts into scan targets', async () => { + it('generates nothing beyond Gradle/sbt/Maven when --dynamic-sbom-inference is used without --auto-manifest', async () => { + mockGenerateRecursiveManifests.mockResolvedValueOnce([ + { + dir: '/repo/service-a', + ecosystem: 'gradle', + factsPath: '/repo/service-a/.socket.facts.json', + status: 'generated', + }, + ]) + + const config = createConfig({ autoManifest: false, targets: ['/repo'] }) + config.reach.dynamicSbomInference = true + + await handleCreateNewScan(config) + + expect(mockGenerateRecursiveManifests).toHaveBeenCalledWith( + expect.objectContaining({ cwd: '/repo' }), + ) + // generateAutoManifest is what would pull in conda and bazel; the flag on + // its own must never reach it. + expect(mockGenerateAutoManifest).not.toHaveBeenCalled() + expect(mockGetPackageFilesForScan).toHaveBeenCalledWith( + ['/repo', '/repo/service-a/.socket.facts.json'], + { size: 1 }, + { + additionalIgnores: [], + config: { projectIgnorePaths: ['fixtures/**'] }, + cwd: '/repo', + }, + ) + }) + + it('suppresses auto-manifest JVM branches and merges recursive facts into scan targets when --dynamic-sbom-inference is combined with --auto-manifest', async () => { mockGenerateRecursiveManifests.mockResolvedValueOnce([ { dir: '/repo/service-a', @@ -242,7 +274,7 @@ describe('handleCreateNewScan excludePaths', () => { { dir: '/repo/service-b', ecosystem: 'maven', status: 'failed' }, ]) - const config = createConfig({ autoManifest: true, targets: ['/repo'] }) + const config = createConfig({ autoManifest: false, targets: ['/repo'] }) config.reach.dynamicSbomInference = true await expect(handleCreateNewScan(config)).rejects.toThrow( @@ -255,7 +287,7 @@ describe('handleCreateNewScan excludePaths', () => { it('aborts when --dynamic-sbom-inference finds no Gradle/sbt/Maven build root', async () => { mockGenerateRecursiveManifests.mockResolvedValueOnce([]) - const config = createConfig({ autoManifest: true, targets: ['/repo'] }) + const config = createConfig({ autoManifest: false, targets: ['/repo'] }) config.reach.dynamicSbomInference = true await expect(handleCreateNewScan(config)).rejects.toThrow( @@ -271,7 +303,7 @@ describe('handleCreateNewScan excludePaths', () => { { dir: '/repo/service-b', ecosystem: 'maven', status: 'skippedDisabled' }, ]) - const config = createConfig({ autoManifest: true, targets: ['/repo'] }) + const config = createConfig({ autoManifest: false, targets: ['/repo'] }) config.reach.dynamicSbomInference = true await handleCreateNewScan(config) @@ -309,7 +341,7 @@ describe('handleCreateNewScan excludePaths', () => { }, ) - const config = createConfig({ autoManifest: true, targets: ['/repo'] }) + const config = createConfig({ autoManifest: false, targets: ['/repo'] }) config.reach.dynamicSbomInference = true config.reach.runReachabilityAnalysis = true diff --git a/src/commands/scan/reachability-flags.mts b/src/commands/scan/reachability-flags.mts index 14b9f15232..9f4dc6e891 100644 --- a/src/commands/scan/reachability-flags.mts +++ b/src/commands/scan/reachability-flags.mts @@ -4,12 +4,6 @@ import { getReachabilityEcosystemChoices } from '../../utils/ecosystem.mts' import type { MeowFlags } from '../../flags.mts' export const reachabilityFlags: MeowFlags = { - dynamicSbomInference: { - type: 'boolean', - default: false, - description: - 'For Gradle, sbt, and Maven: splits reachability analysis per project/module using a Socket facts SBOM (generated directly by each package manager) per build root, instead of one synthetic root. Reachability analysis only; implies --auto-manifest.', - }, reachVersion: { type: 'string', description: `Override the version of @coana-tech/cli used for reachability analysis. Default: ${constants.ENV.INLINED_SOCKET_CLI_COANA_TECH_CLI_VERSION}.`,