Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 12 additions & 6 deletions src/commands/scan/cmd-scan-create.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.`,
)
Expand Down
32 changes: 31 additions & 1 deletion src/commands/scan/cmd-scan-create.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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 \`<cwd>/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
Expand All @@ -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.
Expand Down Expand Up @@ -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',
Expand Down
23 changes: 4 additions & 19 deletions src/commands/scan/cmd-scan-reach.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -89,7 +74,7 @@ async function run(
flags: {
...generalFlags,
...excludePathsFlag,
...reachabilityFlagsForReach,
...reachabilityFlags,
},
help: command =>
`
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand Down
70 changes: 39 additions & 31 deletions src/commands/scan/handle-create-new-scan.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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.')
}
Expand Down
42 changes: 37 additions & 5 deletions src/commands/scan/handle-create-new-scan.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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)
Expand Down Expand Up @@ -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

Expand Down
6 changes: 0 additions & 6 deletions src/commands/scan/reachability-flags.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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}.`,
Expand Down