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
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [Unreleased]

### Added
- Choose which ecosystems `--dynamic-sbom-inference` covers with `--dynamic-sbom-inference-ecosystems` (`maven` for Gradle, sbt and Maven; `nuget` for .NET) on `socket scan create`, `socket scan reach` and `socket fix`. Ecosystems left out keep their manifest files as they are.

### Changed
- With `--dynamic-sbom-inference`, reachability analysis and `socket fix` now take the dependencies of every Gradle, sbt, Maven and .NET build they find from Socket facts only, so results no longer depend on which other builds produced facts.
- `--dynamic-sbom-inference` and its setup now stop with clear next steps when a .NET build cannot be read because no .NET SDK is installed, instead of skipping it. With it, reachability analysis and `socket fix` warn that builds skipped in `socket.json` are left out, and reachability analysis stops with next steps when no build produced Socket facts.
- `socket manifest auto`, `--auto-manifest` and `--dynamic-sbom-inference` (with its setup) now fail a .NET build that has no usable .NET SDK, as they do a Maven build without Maven, instead of skipping it. With `--dynamic-sbom-inference`, reachability analysis and `socket fix` warn that builds skipped in `socket.json` are left out, and reachability analysis stops with next steps when no build produced Socket facts.

## [1.13.1](https://github.com/SocketDev/socket-cli/releases/tag/v1.13.1) - 2026-11-03

Expand Down
2 changes: 1 addition & 1 deletion src/commands/ci/handle-ci.mts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ export async function handleCi(autoManifest: boolean): Promise<void> {
pendingHead: true,
pullRequest: detectCiPullRequestNumber(),
reach: {
dynamicSbomInference: false,
dynamicSbomInference: undefined,
excludePaths: [],
reachAdvisories: [],
reachAnalysisMemoryLimit: '',
Expand Down
1 change: 1 addition & 0 deletions src/commands/fix/cmd-fix.integration.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ describe('socket fix', async () => {
--debug Enable debug logging in the Coana-based Socket Fix CLI invocation.
--disable-external-tool-checks Disable external tool checks during fix analysis.
--dynamic-sbom-inference For Gradle, sbt, Maven and .NET: generate a Socket facts SBOM (produced directly by each package manager) per independent build root, instead of one synthetic root. Fixes are then attributed to the projects/modules that actually resolve each vulnerable dependency. The generated files are removed afterwards.
--dynamic-sbom-inference-ecosystems With --dynamic-sbom-inference: generate Socket facts only for these ecosystems, as either a comma separated value or as multiple flags; the others keep their manifest files as they are. Supported: maven (Gradle, Maven, and sbt) and nuget (.NET). Defaults to all of them.
--ecosystems Limit fix analysis to specific ecosystems. Accepts space- or comma-separated values and is case-insensitive. Defaults to all ecosystems.
--exclude-paths Skip matching paths from the scan entirely: manifests under these paths are not uploaded, and fixes are not applied to workspaces under them. Patterns are anchored micromatch globs matched relative to the target directory (CWD); \`data/postgres/pgdata\` matches that exact path, \`**/pgdata\` matches at any depth. Use this to skip directories the current user cannot read so they do not abort manifest collection. Negation patterns (\`!path\`) are not supported. Accepts a comma-separated value or multiple flags.
--fix-version Override the version of @coana-tech/cli used for fix analysis. Default: <coana-version>.
Expand Down
38 changes: 15 additions & 23 deletions src/commands/fix/cmd-fix.mts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,10 @@ import constants, {
import { commonFlags, outputFlags } from '../../flags.mts'
import { checkCommandInput } from '../../utils/check-input.mts'
import { cmdFlagValueToArray } from '../../utils/cmd.mts'
import { getEcosystemChoicesForMeow } from '../../utils/ecosystem.mts'
import {
getEcosystemChoicesForMeow,
parseEcosystemChoices,
} from '../../utils/ecosystem.mts'
import { getOutputKind } from '../../utils/get-output-kind.mts'
import { meowOrExit } from '../../utils/meow-with-subcommands.mts'
import {
Expand All @@ -33,10 +36,13 @@ import {
import { RangeStyles } from '../../utils/semver.mts'
import { getDefaultOrgSlug } from '../ci/fetch-default-org-slug.mts'
import { assertValidExcludePaths } from '../scan/exclude-paths.mts'
import { DYNAMIC_SBOM_INFERENCE_DESCRIPTION } from '../scan/reachability-flags.mts'
import {
DYNAMIC_SBOM_INFERENCE_DESCRIPTION,
dynamicSbomInferenceEcosystemsFlags,
parseDynamicSbomInferenceFlags,
} from '../scan/reachability-flags.mts'

import type { MeowFlag, MeowFlags } from '../../flags.mts'
import type { PURL_Type } from '../../utils/ecosystem.mts'
import type {
CliCommandConfig,
CliCommandContext,
Expand Down Expand Up @@ -190,6 +196,7 @@ Available styles:
default: false,
description: `${DYNAMIC_SBOM_INFERENCE_DESCRIPTION} Fixes are then attributed to the projects/modules that actually resolve each vulnerable dependency. The generated files are removed afterwards.`,
},
...dynamicSbomInferenceEcosystemsFlags,
ecosystems: {
type: 'string',
default: [],
Expand Down Expand Up @@ -339,7 +346,6 @@ async function run(
autopilot,
debug,
disableExternalToolChecks,
dynamicSbomInference,
ecosystems,
exclude,
excludePaths,
Expand Down Expand Up @@ -367,7 +373,6 @@ async function run(
autopilot: boolean
debug: boolean
disableExternalToolChecks: boolean
dynamicSbomInference: boolean
ecosystems: string[]
exclude: string[]
excludePaths: string[]
Expand Down Expand Up @@ -398,26 +403,13 @@ async function run(

const outputKind = getOutputKind(json, markdown)

// Process comma-separated values for ecosystems flag.
// ALL_ECOSYSTEMS is lowercase, so normalize input for a case-insensitive
// match (mirrors --package-managers behavior).
const ecosystemsRaw = cmdFlagValueToArray(ecosystems).map(s =>
s.toLowerCase(),
)
const dynamicSbomInference = parseDynamicSbomInferenceFlags(cli.flags)

// Validate ecosystem values early, before dry-run check.
const validatedEcosystems: PURL_Type[] = []
const validEcosystemChoices = getEcosystemChoicesForMeow()
for (const ecosystem of ecosystemsRaw) {
if (!validEcosystemChoices.includes(ecosystem)) {
logger.fail(
`Invalid ecosystem: "${ecosystem}". Valid values are: ${joinAnd(validEcosystemChoices)}`,
)
process.exitCode = 1
return
}
validatedEcosystems.push(ecosystem as PURL_Type)
}
const validatedEcosystems = parseEcosystemChoices(
cmdFlagValueToArray(ecosystems),
getEcosystemChoicesForMeow(),
)

// Process and validate package manager values early, before dry-run check.
// Coana normalizes input to uppercase and rejects unknown values, so do the
Expand Down
11 changes: 7 additions & 4 deletions src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ describe('socket fix --dynamic-sbom-inference', () => {
debug: false,
disableExternalToolChecks: false,
disableMajorUpdates: false,
dynamicSbomInference: true,
dynamicSbomInference: ['maven'],
ecosystems: [],
exclude: [],
excludePaths: [],
Expand Down Expand Up @@ -147,6 +147,9 @@ describe('socket fix --dynamic-sbom-inference', () => {
const result = await coanaFix(baseConfig)

expect(result.ok).toBe(true)
expect(mockGenerateSocketFactsForFix).toHaveBeenCalledWith(
expect.objectContaining({ purlTypes: ['maven'] }),
)
expect(uploadManifestFiles).toHaveBeenCalledWith(
'test-org',
['/test/cwd/app/build.gradle', FACTS],
Expand Down Expand Up @@ -196,7 +199,7 @@ describe('socket fix --dynamic-sbom-inference', () => {

const result = await coanaFix({
...baseConfig,
dynamicSbomInference: false,
dynamicSbomInference: undefined,
})

expect(result.ok).toBe(true)
Expand All @@ -211,15 +214,15 @@ describe('socket fix --dynamic-sbom-inference', () => {
FACTS,
])

await coanaFix({ ...baseConfig, dynamicSbomInference: false })
await coanaFix({ ...baseConfig, dynamicSbomInference: undefined })

expect(uploadManifestFiles).toHaveBeenCalledWith('test-org', [FACTS], {
pathsRelativeTo: '/test/cwd',
})
})

it('does not pass the facts restriction without the flag', async () => {
await coanaFix({ ...baseConfig, dynamicSbomInference: false })
await coanaFix({ ...baseConfig, dynamicSbomInference: undefined })

expect(mockGenerateSocketFactsForFix).not.toHaveBeenCalled()
expect(coanaCalls('compute-fixes-and-upgrade-purls')[0]).not.toContain(
Expand Down
12 changes: 7 additions & 5 deletions src/commands/fix/coana-fix.mts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { fetchSupportedScanFileNames } from '../scan/fetch-supported-scan-file-n
import type { GeneratedSocketFacts } from './generated-socket-facts.mts'
import type { FixConfig } from './types.mts'
import type { CResult } from '../../types.mts'
import type { FactsOnlyTypes } from '../../utils/coana.mts'
import type { PURL_Type } from '../../utils/ecosystem.mts'
import type { Spinner } from '@socketsecurity/registry/lib/spinner'

Expand Down Expand Up @@ -283,15 +284,17 @@ function formatNotAppliedFixes(notApplied: NotAppliedFix[]): string {

type GeneratedSocketFactsSlot = {
generated?: GeneratedSocketFacts | undefined
purlTypes: FactsOnlyTypes
tmpDir: string
}

export async function coanaFix(fixConfig: FixConfig): Promise<CoanaFixResult> {
if (!fixConfig.dynamicSbomInference) {
const purlTypes = fixConfig.dynamicSbomInference
if (!purlTypes) {
return await coanaFixWithFacts(fixConfig, undefined)
}
return await withTmpDir('socket-fix-facts-', async tmpDir => {
const slot: GeneratedSocketFactsSlot = { tmpDir }
const slot: GeneratedSocketFactsSlot = { purlTypes, tmpDir }
try {
return await coanaFixWithFacts(fixConfig, slot)
} finally {
Expand Down Expand Up @@ -391,13 +394,12 @@ async function coanaFixWithFacts(
if (factsSlot) {
if (!silence) {
spinner?.stop()
logger.info(
'Generating Socket facts for .NET, Gradle, sbt and Maven builds ...',
)
logger.info('Generating Socket facts for each build ...')
}
factsSlot.generated = await generateSocketFactsForFix({
cwd,
excludePaths,
purlTypes: factsSlot.purlTypes,
findFactsPaths: async () =>
(await findScanFilepaths()).filter(isSocketFactsFile),
tmpDir: factsSlot.tmpDir,
Expand Down
16 changes: 10 additions & 6 deletions src/commands/fix/generated-socket-facts.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@ import path from 'node:path'
import { useOnlySocketFactsArtifactsFlags } from '../../utils/coana.mts'
import { runDynamicSbomInference } from '../scan/run-dynamic-sbom-inference.mts'

import type { PURL_Type } from '../../utils/ecosystem.mts'

export type GeneratedSocketFacts = {
// Coana's flags selecting what the facts files resolve.
artifactsFlags: string[]
paths: string[]
// The facts files' per-project classpaths, written outside the repository.
sidecarFile: string | undefined
sidecarFile: string
remove: () => Promise<void>
restore: () => Promise<void>
}
Expand Down Expand Up @@ -39,11 +41,13 @@ export async function generateSocketFactsForFix({
cwd,
excludePaths,
findFactsPaths,
purlTypes,
tmpDir,
}: {
cwd: string
excludePaths: string[]
findFactsPaths: () => Promise<string[]>
purlTypes: readonly PURL_Type[]
tmpDir: string
}): Promise<GeneratedSocketFacts> {
const existingPaths = await findFactsPaths()
Expand All @@ -57,21 +61,21 @@ export async function generateSocketFactsForFix({
}
let artifactsFlags: string[]
let paths: string[]
let sidecarFile: string | undefined
const sidecarFile = path.join(tmpDir, 'sidecar.json')
try {
const { factsOnlyTypes, factsPaths, resolvedPathsSidecar } =
await runDynamicSbomInference({
consumer: 'fix',
cwd,
excludePaths,
purlTypes,
sbtTmpDir: undefined,
})
artifactsFlags = useOnlySocketFactsArtifactsFlags(factsOnlyTypes)
Comment thread
jfblaa marked this conversation as resolved.
Resolved
paths = factsPaths.map(p => path.resolve(cwd, p))
if (resolvedPathsSidecar) {
sidecarFile = path.join(tmpDir, 'sidecar.json')
await writeFile(sidecarFile, JSON.stringify(resolvedPathsSidecar))
}
// Written even when empty, as for reach; applying fixes looks each facts
// file up in it, so an empty one is the same as none.
await writeFile(sidecarFile, JSON.stringify(resolvedPathsSidecar ?? {}))
} catch (e) {
// A failed build root aborts inference after others wrote their facts.
await reset(await findFactsPaths())
Expand Down
19 changes: 16 additions & 3 deletions src/commands/fix/generated-socket-facts.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ describe('generateSocketFactsForFix', () => {
cwd,
excludePaths: [],
findFactsPaths: async () => [existing, fresh].filter(p => existsSync(p)),
purlTypes: ['maven'],
tmpDir,
})
}
Expand Down Expand Up @@ -75,18 +76,30 @@ describe('generateSocketFactsForFix', () => {
const { artifactsFlags, sidecarFile } = await generate()

expect(mockRunDynamicSbomInference).toHaveBeenCalledWith(
expect.objectContaining({ consumer: 'fix' }),
expect.objectContaining({ consumer: 'fix', purlTypes: ['maven'] }),
)
expect(artifactsFlags).toEqual([
'--use-only-socket-facts-artifacts',
'maven,nuget',
])
expect(path.dirname(sidecarFile!)).toBe(tmpDir)
expect(JSON.parse(await fs.readFile(sidecarFile!, 'utf8'))).toEqual({
expect(path.dirname(sidecarFile)).toBe(tmpDir)
expect(JSON.parse(await fs.readFile(sidecarFile, 'utf8'))).toEqual({
[fresh]: { components: [], projects: [] },
})
})

it('writes an empty sidecar when no build root resolved any paths', async () => {
Comment thread
jfblaa marked this conversation as resolved.
Resolved
mockRunDynamicSbomInference.mockResolvedValue({
factsOnlyTypes: ['maven'],
factsPaths: [],
})

const { artifactsFlags, sidecarFile } = await generate()

expect(artifactsFlags).toEqual(['--maven-use-only-socket-facts'])
expect(JSON.parse(await fs.readFile(sidecarFile, 'utf8'))).toEqual({})
})

it('restores an existing facts file that generation does not rewrite', async () => {
mockRunDynamicSbomInference.mockImplementation(async () => {
await fs.writeFile(fresh, 'generated')
Expand Down
4 changes: 3 additions & 1 deletion src/commands/fix/types.mts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { FactsOnlyTypes } from '../../utils/coana.mts'
import type { PURL_Type } from '../../utils/ecosystem.mts'
import type { RangeStyle } from '../../utils/semver.mts'
import type { Spinner } from '@socketsecurity/registry/lib/spinner'
Expand All @@ -12,7 +13,8 @@ export type FixConfig = {
debug: boolean
disableExternalToolChecks: boolean
disableMajorUpdates: boolean
dynamicSbomInference: boolean
// The ecosystems to generate Socket facts for; undefined when off.
dynamicSbomInference: FactsOnlyTypes | undefined
ecosystems: PURL_Type[]
exclude: string[]
excludePaths: string[]
Expand Down
15 changes: 11 additions & 4 deletions src/commands/manifest/discover-manifest-roots.mts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,13 @@ import micromatch from 'micromatch'

import { logger } from '@socketsecurity/registry/lib/logger'

import { BUILD_FILES, isDotnetSolution } from './scripts/build-tool.mts'
import {
BUILD_FILES,
BUILD_TOOLS,
FACTS_PURL_TYPES,
buildToolsFor,
isDotnetSolution,
} from './scripts/build-tool.mts'
import { listDotnetEntryProjects } from './scripts/run.mts'
import { realpathOrResolved } from '../../utils/fs.mts'
import { globWithGitIgnore } from '../../utils/glob.mts'
Expand All @@ -14,11 +20,10 @@ import { excludePathToScanIgnores } from '../scan/exclude-paths.mts'
import type { BuildTool } from './scripts/build-tool.mts'
import type { SocketFactsSbomProject } from './scripts/facts.mts'
import type { ManifestScriptOptions } from './scripts/run.mts'
import type { PURL_Type } from '../../utils/ecosystem.mts'
import type { SocketJson } from '../../utils/socket-json.mts'
import type { SocketYml } from '@socketsecurity/config'

const BUILD_TOOLS: BuildTool[] = ['dotnet', 'gradle', 'maven', 'sbt']

const BUILD_FILE_MATCHERS = new Map(
BUILD_TOOLS.map(tool => {
const files = BUILD_FILES[tool]
Expand Down Expand Up @@ -222,17 +227,19 @@ export function sortByDepthThenPath<T extends BuildRoot>(
export async function findBuildToolCandidates({
cwd,
excludePaths,
purlTypes = FACTS_PURL_TYPES,
sockJson,
socketConfig,
}: {
cwd: string
excludePaths?: string[] | undefined
purlTypes?: readonly PURL_Type[] | undefined
// The socket.yml a scan applies to its upload, so a build file a scan would
// not upload is not built either.
socketConfig: SocketYml | undefined
sockJson: SocketJson
}): Promise<Map<BuildTool, BuildRoot[]>> {
const enabledTools = BUILD_TOOLS.filter(
const enabledTools = buildToolsFor(purlTypes).filter(
tool => !sockJson.defaults?.manifest?.[tool]?.disabled,
)
const result = new Map<BuildTool, BuildRoot[]>()
Expand Down
Loading
Loading