Skip to content

socket-cli: --dynamic-sbom-inference flag for per-workspace Maven reachability - #1451

Merged
Jeppe Fredsgaard Blaabjerg (jfblaa) merged 4 commits into
v1.xfrom
jfblaa/rea-687-socket-cli-dynamic-sbom-inference-scan-flag-for-per
Jul 31, 2026
Merged

socket-cli: --dynamic-sbom-inference flag for per-workspace Maven reachability#1451
Jeppe Fredsgaard Blaabjerg (jfblaa) merged 4 commits into
v1.xfrom
jfblaa/rea-687-socket-cli-dynamic-sbom-inference-scan-flag-for-per

Conversation

@jfblaa

@jfblaa Jeppe Fredsgaard Blaabjerg (jfblaa) commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Adds a hidden --dynamic-sbom-inference flag to socket scan create (and socket scan reach) that forces --auto-manifest on and passes --maven-use-only-root-socket-facts through to the Coana CLI, so Coana consumes pre-generated per-workspace Socket facts instead of resolving the full Maven reactor itself.
  • Fixes assemble.mts so a dependency edge that resolves to a first-party project reports that project's own build source/target roots instead of a classpath-resolved artifact path that may not exist if the module hasn't been built yet.

Status: draft, not mergeable yet

This PR is blocked until the Coana CLI supports --maven-use-only-root-socket-facts and @coana-tech/cli is bumped + published in this repo. Until then it can only be exercised via a local Coana build (SOCKET_CLI_COANA_LOCAL_PATH).

Linear: REA-687

Test plan

  • pnpm check:tsc / pnpm check:lint clean
  • pnpm build (rollup + tsgo) succeeds
  • Relevant unit tests pass (perform-reachability-analysis, handle-scan-reach, handle-create-new-scan, exclude-paths)
  • End-to-end --reach --dynamic-sbom-inference smoke test once Coana CLI is published

Note

Medium Risk
Changes Maven reachability inputs and Coana CLI wiring; wrong artifact paths could skew analysis, but the flag is hidden and defaults off.

Overview
Adds a hidden --dynamic-sbom-inference flag on socket scan create and socket scan reach. When set (with --reach), it turns on --auto-manifest so per-workspace Socket facts are generated, and forwards --maven-use-only-root-socket-facts to Coana so Maven reachability uses those pre-generated facts instead of resolving the full reactor inside Coana.

assemble.mts now maps first-party Maven modules to only the project’s own build targets (not merged with classpath artifact paths from dependency nodes), and sorts sources—so reachability points at module source roots that exist before a local build.

Bumps @coana-tech/cli to 15.9.9 and releases 1.1.151.

Reviewed by Cursor Bugbot for commit 1d0c692. Configure here.

…e Maven reachability

Adds a hidden --dynamic-sbom-inference flag to `socket scan create` and
`socket scan reach`. Setting it forces --auto-manifest on to generate
per-workspace Socket facts, and passes --maven-use-only-root-socket-facts
to the Coana CLI so it consumes those pre-generated per-workspace facts
instead of resolving the full Maven reactor itself.

Also fixes assemble.mts: when a dependency edge resolves to a first-party
project (a subproject referenced in dependency position), the sidecar now
reports that project's own build source/target roots instead of the
classpath-resolved artifact path, since the corresponding jar may not
exist if the module hasn't been built yet. Sources are now sorted for
determinism, matching targets.

Blocked on a Coana CLI release supporting --maven-use-only-root-socket-facts.

Linear: REA-687

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

Bugbot Autofix prepared a fix for the issue found in the latest run.

  • ✅ Fixed: Reach skips auto-manifest for SBOM flag
    • Confirmed real: scan reach now runs generateAutoManifest with computeArtifactsSidecar and forwards resolvedPathsSidecar when --dynamic-sbom-inference is set, matching scan create.

Create PR

Or push these changes by commenting:

@cursor push 53b1f33147
Preview (53b1f33147)
diff --git a/src/commands/scan/handle-scan-reach.mts b/src/commands/scan/handle-scan-reach.mts
--- a/src/commands/scan/handle-scan-reach.mts
+++ b/src/commands/scan/handle-scan-reach.mts
@@ -10,9 +10,13 @@
 import { checkCommandInput } from '../../utils/check-input.mts'
 import { findSocketYmlSync } from '../../utils/config.mts'
 import { getPackageFilesForScan } from '../../utils/path-resolve.mts'
+import { readOrDefaultSocketJson } from '../../utils/socket-json.mts'
+import { detectManifestActions } from '../manifest/detect-manifest-actions.mts'
+import { generateAutoManifest } from '../manifest/generate_auto_manifest.mts'
 
 import type { ReachabilityOptions } from './perform-reachability-analysis.mts'
 import type { OutputKind } from '../../types.mts'
+import type { ResolvedPathsSidecar } from '../manifest/scripts/sidecar.mts'
 
 export type HandleScanReachConfig = {
   cwd: string
@@ -35,6 +39,33 @@
 }: HandleScanReachConfig) {
   const { spinner } = constants
 
+  // --dynamic-sbom-inference implies auto-manifest so Coana can consume
+  // per-workspace Socket facts via --maven-use-only-root-socket-facts.
+  let scanTargets = targets
+  let resolvedPathsSidecar: ResolvedPathsSidecar | undefined
+  if (reachabilityOptions.dynamicSbomInference) {
+    logger.info('Auto-generating manifest files ...')
+    const sockJson = readOrDefaultSocketJson(cwd)
+    const detected = await detectManifestActions(sockJson, cwd)
+    const autoManifestResult = await generateAutoManifest({
+      computeArtifactsSidecar: true,
+      cwd,
+      detected,
+      excludePaths: reachabilityOptions.excludePaths,
+      outputKind,
+      verbose: false,
+    })
+    resolvedPathsSidecar = autoManifestResult.resolvedPathsSidecar
+    if (autoManifestResult.generatedFiles.length) {
+      scanTargets = Array.from(
+        new Set([...targets, ...autoManifestResult.generatedFiles]),
+      )
+    }
+    logger.info(
+      'Auto-generation finished. Proceeding with reachability analysis.',
+    )
+  }
+
   // Get supported file names.
   const supportedFilesCResult = await fetchSupportedScanFileNames({
     orgSlug,
@@ -68,11 +99,15 @@
       target: targets[0]!,
     })
 
-  const packagePaths = await getPackageFilesForScan(targets, supportedFiles, {
-    additionalIgnores: additionalScaIgnores,
-    config: socketConfig,
-    cwd,
-  })
+  const packagePaths = await getPackageFilesForScan(
+    scanTargets,
+    supportedFiles,
+    {
+      additionalIgnores: additionalScaIgnores,
+      config: socketConfig,
+      cwd,
+    },
+  )
 
   spinner.successAndStop(
     `Found ${packagePaths.length} ${pluralize('manifest file', packagePaths.length)} for reachability analysis.`,
@@ -102,6 +137,7 @@
     outputPath,
     packagePaths,
     reachabilityOptions: mergedReachabilityOptions,
+    resolvedPathsSidecar,
     spinner,
     target: targets[0]!,
     uploadManifests: true,

diff --git a/src/commands/scan/handle-scan-reach.test.mts b/src/commands/scan/handle-scan-reach.test.mts
--- a/src/commands/scan/handle-scan-reach.test.mts
+++ b/src/commands/scan/handle-scan-reach.test.mts
@@ -4,25 +4,33 @@
 
 const {
   mockCheckCommandInput,
+  mockDetectManifestActions,
   mockFetchSupportedScanFileNames,
   mockFinalizeTier1Scan,
   mockFindSocketYmlSync,
+  mockGenerateAutoManifest,
   mockGetPackageFilesForScan,
+  mockLoggerInfo,
   mockLoggerSuccess,
   mockLoggerWarn,
   mockOutputScanReach,
   mockPerformReachabilityAnalysis,
+  mockReadOrDefaultSocketJson,
   mockSentryInternalsSymbol,
 } = vi.hoisted(() => ({
   mockCheckCommandInput: vi.fn(),
+  mockDetectManifestActions: vi.fn(),
   mockFetchSupportedScanFileNames: vi.fn(),
   mockFinalizeTier1Scan: vi.fn(),
   mockFindSocketYmlSync: vi.fn(),
+  mockGenerateAutoManifest: vi.fn(),
   mockGetPackageFilesForScan: vi.fn(),
+  mockLoggerInfo: vi.fn(),
   mockLoggerSuccess: vi.fn(),
   mockLoggerWarn: vi.fn(),
   mockOutputScanReach: vi.fn(),
   mockPerformReachabilityAnalysis: vi.fn(),
+  mockReadOrDefaultSocketJson: vi.fn(),
   mockSentryInternalsSymbol: Symbol('kInternalsSymbol'),
 }))
 
@@ -72,8 +80,21 @@
   getPackageFilesForScan: mockGetPackageFilesForScan,
 }))
 
+vi.mock('../../utils/socket-json.mts', () => ({
+  readOrDefaultSocketJson: mockReadOrDefaultSocketJson,
+}))
+
+vi.mock('../manifest/detect-manifest-actions.mts', () => ({
+  detectManifestActions: mockDetectManifestActions,
+}))
+
+vi.mock('../manifest/generate_auto_manifest.mts', () => ({
+  generateAutoManifest: mockGenerateAutoManifest,
+}))
+
 vi.mock('@socketsecurity/registry/lib/logger', () => ({
   logger: {
+    info: mockLoggerInfo,
     success: mockLoggerSuccess,
     warn: mockLoggerWarn,
   },
@@ -83,6 +104,7 @@
   beforeEach(() => {
     vi.clearAllMocks()
     mockCheckCommandInput.mockReturnValue(true)
+    mockDetectManifestActions.mockResolvedValue({ count: 0 })
     mockFetchSupportedScanFileNames.mockResolvedValue({
       ok: true,
       data: { npm: { packageJson: { pattern: 'package.json' } } },
@@ -92,6 +114,7 @@
       ok: true,
       data: { parsed: { projectIgnorePaths: ['vendor/**'] } },
     })
+    mockGenerateAutoManifest.mockResolvedValue({ generatedFiles: [] })
     mockGetPackageFilesForScan.mockResolvedValue(['package.json'])
     mockPerformReachabilityAnalysis.mockResolvedValue({
       ok: true,
@@ -100,6 +123,7 @@
         tier1ReachabilityScanId: undefined,
       },
     })
+    mockReadOrDefaultSocketJson.mockReturnValue({})
   })
 
   it('applies excludePaths to manifest discovery and reachability analysis', async () => {
@@ -137,6 +161,7 @@
       targets: ['.'],
     })
 
+    expect(mockGenerateAutoManifest).not.toHaveBeenCalled()
     expect(mockGetPackageFilesForScan).toHaveBeenCalledWith(
       ['.'],
       { npm: { packageJson: { pattern: 'package.json' } } },
@@ -151,10 +176,86 @@
         reachabilityOptions: expect.objectContaining({
           reachExcludePaths: ['node_modules', 'tests', 'packages/*'],
         }),
+        resolvedPathsSidecar: undefined,
       }),
     )
   })
 
+  it('runs auto-manifest and forwards sidecar when dynamicSbomInference is set', async () => {
+    const resolvedPathsSidecar = [
+      {
+        classifier: null,
+        ext: 'jar',
+        group: 'g',
+        name: 'n',
+        sources: [],
+        targets: [],
+        version: '1',
+      },
+    ]
+    mockGenerateAutoManifest.mockResolvedValueOnce({
+      generatedFiles: ['/repo/.socket.facts.json'],
+      resolvedPathsSidecar,
+    })
+
+    const reachabilityOptions = {
+      dynamicSbomInference: true,
+      excludePaths: ['tests'],
+      reachAnalysisMemoryLimit: '8192',
+      reachAnalysisTimeout: '',
+      reachConcurrency: 1,
+      reachContinueOnAnalysisErrors: false,
+      reachContinueOnInstallErrors: false,
+      reachContinueOnMissingLockFiles: false,
+      reachContinueOnNoSourceFiles: false,
+      reachDebug: false,
+      reachDetailedAnalysisLogFile: false,
+      reachDisableAnalytics: false,
+      reachDisableExternalToolChecks: false,
+      reachEcosystems: [],
+      reachEnableAnalysisSplitting: false,
+      reachExcludePaths: [],
+      reachLazyMode: false,
+      reachRetainFactsFile: false,
+      reachSkipCache: false,
+      reachUseOnlyPregeneratedSboms: false,
+      reachVersion: undefined,
+    }
+
+    await handleScanReach({
+      cwd: '/repo',
+      interactive: false,
+      orgSlug: 'fakeOrg',
+      outputKind: 'text',
+      outputPath: '',
+      reachabilityOptions,
+      targets: ['.'],
+    })
+
+    expect(mockGenerateAutoManifest).toHaveBeenCalledWith({
+      computeArtifactsSidecar: true,
+      cwd: '/repo',
+      detected: { count: 0 },
+      excludePaths: ['tests'],
+      outputKind: 'text',
+      verbose: false,
+    })
+    expect(mockGetPackageFilesForScan).toHaveBeenCalledWith(
+      ['.', '/repo/.socket.facts.json'],
+      { npm: { packageJson: { pattern: 'package.json' } } },
+      {
+        additionalIgnores: ['tests', 'tests/**'],
+        config: { projectIgnorePaths: ['vendor/**'] },
+        cwd: '/repo',
+      },
+    )
+    expect(mockPerformReachabilityAnalysis).toHaveBeenCalledWith(
+      expect.objectContaining({
+        resolvedPathsSidecar,
+      }),
+    )
+  })
+
   it('translates excludePaths from the scan root for nested targets', async () => {
     const reachabilityOptions = {
       dynamicSbomInference: false,

You can send follow-ups to the cloud agent here.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 1d0c692. Configure here.

Comment thread src/commands/scan/cmd-scan-reach.mts Outdated
socket scan reach never runs auto-manifest, so honoring the flag there
passed Coana --maven-use-only-root-socket-facts without the
per-workspace Socket facts it depends on ever being generated,
producing incorrect or failed reachability analysis. The flag is
hidden/internal and scan reach is a hidden command, so this is
disabled quietly rather than adding a new validation error.
@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​coana-tech/​cli@​15.9.9961008098100

View full report

@jfblaa
Jeppe Fredsgaard Blaabjerg (jfblaa) merged commit 9419993 into v1.x Jul 31, 2026
4 checks passed
@jfblaa
Jeppe Fredsgaard Blaabjerg (jfblaa) deleted the jfblaa/rea-687-socket-cli-dynamic-sbom-inference-scan-flag-for-per branch July 31, 2026 07:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants