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
24 changes: 13 additions & 11 deletions src/commands/scan/cmd-scan-reach.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ import { logger } from '@socketsecurity/registry/lib/logger'

import { assertValidExcludePaths } from './exclude-paths.mts'
import { handleScanReach } from './handle-scan-reach.mts'
import { excludePathsFlag, reachabilityFlags } from './reachability-flags.mts'
import {
DYNAMIC_SBOM_INFERENCE_DESCRIPTION,
excludePathsFlag,
reachabilityFlags,
} from './reachability-flags.mts'
import { suggestTarget } from './suggest_target.mts'
import { validateReachabilityTarget } from './validate-reachability-target.mts'
import constants from '../../constants.mts'
Expand Down Expand Up @@ -33,18 +37,15 @@ 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.
// `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.
const reachabilityFlagsForReach: MeowFlags = {
...reachabilityFlags,
dynamicSbomInference: {
type: 'boolean',
default: false,
hidden: true,
description: reachabilityFlags['dynamicSbomInference']!.description,
description: `${DYNAMIC_SBOM_INFERENCE_DESCRIPTION} Each discovered build root is built first to generate its SBOM.`,
},
}

Expand Down Expand Up @@ -117,6 +118,7 @@ async function run(
$ ${command}
$ ${command} ./proj
$ ${command} ./proj --reach-ecosystems npm,pypi
$ ${command} ./monorepo --dynamic-sbom-inference
$ ${command} --output custom-report.json
$ ${command} ./proj --output ./reports/analysis.json
`,
Expand All @@ -131,6 +133,7 @@ async function run(

const {
cwd: cwdOverride,
dynamicSbomInference,
interactive = true,
json,
markdown,
Expand All @@ -156,6 +159,7 @@ async function run(
reachVersion,
} = cli.flags as {
cwd: string
dynamicSbomInference: boolean
interactive: boolean
json: boolean
markdown: boolean
Expand Down Expand Up @@ -282,9 +286,7 @@ 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.
dynamicSbomInference: false,
dynamicSbomInference: Boolean(dynamicSbomInference),
excludePaths,
reachAnalysisMemoryLimit,
reachAnalysisTimeout,
Expand Down
2 changes: 2 additions & 0 deletions src/commands/scan/cmd-scan-reach.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +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.
--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 Expand Up @@ -68,6 +69,7 @@ describe('socket scan reach', async () => {
$ socket scan reach
$ socket scan reach ./proj
$ socket scan reach ./proj --reach-ecosystems npm,pypi
$ socket scan reach ./monorepo --dynamic-sbom-inference
$ socket scan reach --output custom-report.json
$ socket scan reach ./proj --output ./reports/analysis.json"
`)
Expand Down
53 changes: 8 additions & 45 deletions src/commands/scan/handle-create-new-scan.mts
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,23 @@ import { finalizeTier1Scan } from './finalize-tier1-scan.mts'
import { handleScanReport } from './handle-scan-report.mts'
import { outputCreateNewScan } from './output-create-new-scan.mts'
import { performReachabilityAnalysis } from './perform-reachability-analysis.mts'
import { runDynamicSbomInference } from './run-dynamic-sbom-inference.mts'
import constants from '../../constants.mts'
import { checkCommandInput } from '../../utils/check-input.mts'
import { compressSocketFactsForUpload } from '../../utils/coana.mts'
import { findSocketYmlSync } from '../../utils/config.mts'
import { InputError } from '../../utils/errors.mts'
import { withTmpDir } from '../../utils/fs.mts'
import { getPackageFilesForScan } from '../../utils/path-resolve.mts'
import { readOrDefaultSocketJson } from '../../utils/socket-json.mts'
import { socketDocsLink } from '../../utils/terminal-link.mts'
import { detectManifestActions } from '../manifest/detect-manifest-actions.mts'
import { generateRecursiveManifests } from '../manifest/generate-recursive-manifests.mts'
import { generateAutoManifest } from '../manifest/generate_auto_manifest.mts'
import {
hasSidecarEntries,
mergeResolvedPathsSidecars,
serializeSidecar,
} from '../manifest/scripts/sidecar.mts'
import { mergeResolvedPathsSidecars } from '../manifest/scripts/sidecar.mts'

import type { ReachabilityOptions } from './perform-reachability-analysis.mts'
import type { REPORT_LEVEL } from './types.mts'
import type { OutputKind } from '../../types.mts'
import type {
ResolvedPathsSidecar,
SidecarAccumulator,
} from '../manifest/scripts/sidecar.mts'
import type { ResolvedPathsSidecar } from '../manifest/scripts/sidecar.mts'
import type { Remap } from '@socketsecurity/registry/lib/objects'
import type { SocketSdkSuccessResult } from '@socketsecurity/sdk'

Expand Down Expand Up @@ -172,50 +164,21 @@ export async function handleCreateNewScan({
detected.sbt = false
detected.maven = false

const sidecarAcc: SidecarAccumulator | undefined =
reach.runReachabilityAnalysis ? new Map() : undefined
const outcomes = await generateRecursiveManifests({
const dynamicResult = await runDynamicSbomInference({
cwd,
excludePaths: reach.excludePaths,
// sbt's Scala toolchain lives under its shared global base;
// withFiles' resolved paths point into it, so when reachability
// will consume them afterward, reuse manifestTmpDir (kept alive
// until reach finishes below) instead of letting this call clean
// until reach finishes below) instead of letting the call clean
// its own ephemeral base up before reach ever reads those paths.
sbtTmpDir: reach.runReachabilityAnalysis ? manifestTmpDir : undefined,
sidecarAcc,
verbose: false,
sbtTmpDir: manifestTmpDir,
withFiles: reach.runReachabilityAnalysis,
})
// No candidates discovered at all (distinct from candidates that were
// found but produced no generated facts - empty/skippedDisabled are
// already warned about elsewhere and are not this kind of mistake).
if (!outcomes.length) {
throw new InputError(
[
'No Gradle, sbt, or Maven build root was found.',
'',
'- Remove --dynamic-sbom-inference; it only applies to these ecosystems.',
'- Make sure to run it from the correct dir (use --cwd to target another dir).',
].join('\n'),
)
}
// Fail loud rather than silently upload a partial multi-root scan:
// matches handleManifestDynamicSbomInference's own check.
if (outcomes.some(o => o.status === 'failed')) {
throw new InputError(
'One or more independent build roots failed to generate Socket facts; aborting (see the errors above).',
)
}
const generatedFactsPaths = outcomes
.filter(o => o.status === 'generated')
.map(o => o.factsPath!)
scanTargets = Array.from(
new Set([...scanTargets, ...generatedFactsPaths]),
new Set([...scanTargets, ...dynamicResult.factsPaths]),
)
if (sidecarAcc && hasSidecarEntries(sidecarAcc)) {
resolvedPathsSidecar = serializeSidecar(sidecarAcc)
}
resolvedPathsSidecar = dynamicResult.resolvedPathsSidecar
}

const autoManifestResult = await generateAutoManifest({
Expand Down
74 changes: 60 additions & 14 deletions src/commands/scan/handle-scan-reach.mts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@ import { fetchSupportedScanFileNames } from './fetch-supported-scan-file-names.m
import { finalizeTier1Scan } from './finalize-tier1-scan.mts'
import { outputScanReach } from './output-scan-reach.mts'
import { performReachabilityAnalysis } from './perform-reachability-analysis.mts'
import { runDynamicSbomInference } from './run-dynamic-sbom-inference.mts'
import constants from '../../constants.mts'
import { checkCommandInput } from '../../utils/check-input.mts'
import { findSocketYmlSync } from '../../utils/config.mts'
import { withTmpDir } from '../../utils/fs.mts'
import { getPackageFilesForScan } from '../../utils/path-resolve.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
Expand All @@ -24,17 +27,42 @@ export type HandleScanReachConfig = {
targets: string[]
}

export async function handleScanReach({
cwd,
interactive: _interactive,
orgSlug,
outputKind,
outputPath,
reachabilityOptions,
targets,
}: HandleScanReachConfig) {
async function runScanReach(
{
cwd,
interactive: _interactive,
orgSlug,
outputKind,
outputPath,
reachabilityOptions,
targets,
}: HandleScanReachConfig,
sbtTmpDir: string | undefined,
) {
const { spinner } = constants

// Extra discovery targets beyond the user's own; the reachability target
// itself stays `targets[0]`.
let scanTargets = targets
// Sidecar forwarded to reachability; populated by dynamic SBOM inference.
let resolvedPathsSidecar: ResolvedPathsSidecar | undefined

if (reachabilityOptions.dynamicSbomInference) {
logger.info(
'Generating Socket facts for each Gradle, sbt, and Maven build root ...',
)
const dynamicResult = await runDynamicSbomInference({
cwd,
excludePaths: reachabilityOptions.excludePaths,
sbtTmpDir,
withFiles: true,
})
scanTargets = Array.from(
new Set([...scanTargets, ...dynamicResult.factsPaths]),
)
resolvedPathsSidecar = dynamicResult.resolvedPathsSidecar
}

// Get supported file names.
const supportedFilesCResult = await fetchSupportedScanFileNames({
orgSlug,
Expand Down Expand Up @@ -68,11 +96,15 @@ export async function handleScanReach({
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.`,
Expand Down Expand Up @@ -102,6 +134,7 @@ export async function handleScanReach({
outputPath,
packagePaths,
reachabilityOptions: mergedReachabilityOptions,
resolvedPathsSidecar,
spinner,
target: targets[0]!,
uploadManifests: true,
Expand All @@ -127,3 +160,16 @@ export async function handleScanReach({

await outputScanReach(result, { cwd, outputKind, outputPath })
}

export async function handleScanReach(
config: HandleScanReachConfig,
): Promise<void> {
// sbt provisions its Scala toolchain under the directory passed as its
// isolated global base; the sidecar's artifactPaths point into it, so it
// must stay on disk until the reachability analysis has consumed them.
return config.reachabilityOptions.dynamicSbomInference
? await withTmpDir('socket-dynamic-sbom-inference-', tmpDir =>
runScanReach(config, tmpDir),
)
: await runScanReach(config, undefined)
}
Loading