Skip to content
Merged
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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,10 @@ test/fixtures/commands/fix/e2e-test-py-temp-*
/src/commands/manifest/scripts/test/sbt-compat/project/localrepo/
/src/commands/manifest/scripts/test/sbt-compat/project/records.tsv
/src/commands/manifest/scripts/test/sbt-compat/project/target/

# Generated when the dynamic SBOM inference tests walk their fixtures
# (test/fixtures/commands/manifest/dynamic-sbom-inference). Those fixtures are
# build-tool marker files only, so any build output under them is debris.
/test/fixtures/commands/manifest/dynamic-sbom-inference/**/.gradle/
/test/fixtures/commands/manifest/dynamic-sbom-inference/**/build/
/test/fixtures/commands/manifest/dynamic-sbom-inference/**/target/
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
### Changed
- Updated the Coana CLI to v `15.10.33`.

### 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.165](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.165) - 2026-09-02

### Changed
Expand Down
25 changes: 17 additions & 8 deletions src/commands/scan/cmd-scan-create.mts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import { logger } from '@socketsecurity/registry/lib/logger'
import { assertValidExcludePaths } from './exclude-paths.mts'
import { handleCreateNewScan } from './handle-create-new-scan.mts'
import { outputCreateNewScan } from './output-create-new-scan.mts'
import { excludePathsFlag, reachabilityFlags } from './reachability-flags.mts'
import {
DYNAMIC_SBOM_INFERENCE_DESCRIPTION,
excludePathsFlag,
reachabilityFlags,
} from './reachability-flags.mts'
import {
isOmittedReachValue,
reachMemoryLimitToMb,
Expand Down Expand Up @@ -55,7 +59,7 @@ const generalFlags: MeowFlags = {
autoManifest: {
type: 'boolean',
description:
'Run `socket manifest auto` before collecting manifest files. This is necessary for languages like Scala, Gradle, and Kotlin, See `socket manifest auto --help`.',
'Run `socket manifest auto` before collecting manifest files, which generates them for ecosystems that need their build tool to resolve dependencies, such as Scala, Gradle, and Kotlin. See `socket manifest auto --help`. For Gradle, sbt, and Maven, --dynamic-sbom-inference generates a Socket facts SBOM per build root instead.',
},
branch: {
type: 'string',
Expand Down Expand Up @@ -92,6 +96,11 @@ 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: `${DYNAMIC_SBOM_INFERENCE_DESCRIPTION} Combine with --reach to split the reachability analysis per project/module.`,
},
interactive: {
type: 'boolean',
default: true,
Expand Down Expand Up @@ -355,11 +364,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 +459,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
34 changes: 32 additions & 2 deletions src/commands/scan/cmd-scan-create.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -33,13 +33,14 @@ describe('socket scan create', async () => {
- Permissions: full-scans:create

Options
--auto-manifest Run \`socket manifest auto\` before collecting manifest files. This is necessary for languages like Scala, Gradle, and Kotlin, See \`socket manifest auto --help\`.
--auto-manifest Run \`socket manifest auto\` before collecting manifest files, which generates them for ecosystems that need their build tool to resolve dependencies, such as Scala, Gradle, and Kotlin. See \`socket manifest auto --help\`. For Gradle, sbt, and Maven, --dynamic-sbom-inference generates a Socket facts SBOM per build root instead.
--branch Branch name
--commit-hash Commit hash
--commit-message Commit message
--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
8 changes: 4 additions & 4 deletions src/commands/scan/cmd-scan-reach.mts
Original file line number Diff line number Diff line change
Expand Up @@ -37,15 +37,15 @@ const description = 'Compute full application reachability'

const hidden = true

// `scan create` gets its per-build-root facts from --auto-manifest, a flag
// this command doesn't have; here the build tools are run directly instead,
// so the flag's description differs.
// Not in reachabilityFlags: on `scan create` it is a general flag the --reach
// guard must not reject. Reachability is always on here, so the description
// drops that command's --reach caveat.
const reachabilityFlagsForReach: MeowFlags = {
...reachabilityFlags,
dynamicSbomInference: {
type: 'boolean',
default: false,
description: `${DYNAMIC_SBOM_INFERENCE_DESCRIPTION} Each discovered build root is built first to generate its SBOM.`,
description: `${DYNAMIC_SBOM_INFERENCE_DESCRIPTION} The reachability analysis is split per project/module accordingly.`,
},
}

Expand Down
2 changes: 1 addition & 1 deletion src/commands/scan/cmd-scan-reach.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ describe('socket scan reach', async () => {
--output Path to write the reachability report to (must end with .json). Defaults to .socket.facts.json in the current working directory.

Reachability Options
--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. Each discovered build root is built first to generate its SBOM.
--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. The reachability analysis is split per project/module accordingly.
--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.
--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.
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 @@ -149,21 +149,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 dynamicResult = await runDynamicSbomInference({
cwd,
excludePaths: reach.excludePaths,
Expand All @@ -181,27 +175,41 @@ export async function handleCreateNewScan({
resolvedPathsSidecar = dynamicResult.resolvedPathsSidecar
}

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
13 changes: 4 additions & 9 deletions src/commands/scan/reachability-flags.mts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,13 @@ import { getReachabilityEcosystemChoices } from '../../utils/ecosystem.mts'

import type { MeowFlags } from '../../flags.mts'

// Shared prefix so each command can append how it obtains the per-build-root
// SBOMs, which differs: `scan create` piggybacks on --auto-manifest, while
// `scan reach` runs the build tools itself.
// Shared prefix so each command can append what the SBOMs are used for, which
// differs: reachability is optional on `scan create` and always on for
// `scan reach`.
export const DYNAMIC_SBOM_INFERENCE_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.'
'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.'

export const reachabilityFlags: MeowFlags = {
dynamicSbomInference: {
type: 'boolean',
default: false,
description: `${DYNAMIC_SBOM_INFERENCE_DESCRIPTION} 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