diff --git a/CHANGELOG.md b/CHANGELOG.md index 02614c3ad..de7a4b589 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 41564b50c..80e771607 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 ddaaa47cd..03d456c03 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 70c280842..94097c2bc 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 1b2109d68..d6d1ff957 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 fdd5fe5f9..fb47a65be 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 14b9f1523..9f4dc6e89 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}.`,