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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ 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.
Expand Down
2 changes: 2 additions & 0 deletions src/commands/ci/handle-ci.mts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
gitBranch,
} from '../../utils/git.mts'
import { serializeResultJson } from '../../utils/serialize-result-json.mts'
import { FACTS_PURL_TYPES } from '../manifest/scripts/build-tool.mts'
import { handleCreateNewScan } from '../scan/handle-create-new-scan.mts'

/**
Expand Down Expand Up @@ -66,6 +67,7 @@ export async function handleCi(autoManifest: boolean): Promise<void> {
pullRequest: detectCiPullRequestNumber(),
reach: {
dynamicSbomInference: false,
dynamicSbomInferenceEcosystems: FACTS_PURL_TYPES,
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, sbt and Maven) and nuget (.NET). Defaults to both.
--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
22 changes: 21 additions & 1 deletion src/commands/fix/cmd-fix.mts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,11 @@ 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,
parseDynamicSbomInferenceEcosystems,
} from '../scan/reachability-flags.mts'

import type { MeowFlag, MeowFlags } from '../../flags.mts'
import type { PURL_Type } from '../../utils/ecosystem.mts'
Expand Down Expand Up @@ -190,6 +194,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 @@ -398,6 +403,13 @@ async function run(

const outputKind = getOutputKind(json, markdown)

const dynamicSbomInferenceEcosystemsRaw = cmdFlagValueToArray(
cli.flags['dynamicSbomInferenceEcosystems'],
)
const dynamicSbomInferenceEcosystems = parseDynamicSbomInferenceEcosystems(
dynamicSbomInferenceEcosystemsRaw,
)

// Process comma-separated values for ecosystems flag.
// ALL_ECOSYSTEMS is lowercase, so normalize input for a case-insensitive
// match (mirrors --package-managers behavior).
Expand Down Expand Up @@ -446,6 +458,13 @@ async function run(

const wasValidInput = checkCommandInput(
outputKind,
{
nook: true,
test: dynamicSbomInference || !dynamicSbomInferenceEcosystemsRaw.length,
message:
'--dynamic-sbom-inference-ecosystems requires --dynamic-sbom-inference',
fail: 'add --dynamic-sbom-inference',
},
{
test: RangeStyles.includes(rangeStyle),
message: `Expecting range style of ${joinOr(RangeStyles)}`,
Expand Down Expand Up @@ -538,6 +557,7 @@ async function run(
disableExternalToolChecks,
disableMajorUpdates,
dynamicSbomInference,
dynamicSbomInferenceEcosystems,
ecosystems: validatedEcosystems,
exclude: excludePatterns,
excludePaths: excludePathsPatterns,
Expand Down
4 changes: 4 additions & 0 deletions src/commands/fix/coana-fix-dynamic-sbom-inference.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ describe('socket fix --dynamic-sbom-inference', () => {
disableExternalToolChecks: false,
disableMajorUpdates: false,
dynamicSbomInference: true,
dynamicSbomInferenceEcosystems: ['maven'],
ecosystems: [],
exclude: [],
excludePaths: [],
Expand Down Expand Up @@ -147,6 +148,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
5 changes: 2 additions & 3 deletions src/commands/fix/coana-fix.mts
Original file line number Diff line number Diff line change
Expand Up @@ -391,13 +391,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: fixConfig.dynamicSbomInferenceEcosystems,
findFactsPaths: async () =>
(await findScanFilepaths()).filter(isSocketFactsFile),
tmpDir: factsSlot.tmpDir,
Expand Down
5 changes: 5 additions & 0 deletions src/commands/fix/generated-socket-facts.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ 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[]
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 @@ -64,6 +68,7 @@ export async function generateSocketFactsForFix({
consumer: 'fix',
cwd,
excludePaths,
purlTypes,
sbtTmpDir: undefined,
})
artifactsFlags = useOnlySocketFactsArtifactsFlags(factsOnlyTypes)

Choose a reason for hiding this comment

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

This repeats the sidecar and facts-only flag assembly from performReachabilityAnalysis, and the two copies already differ. Reach writes a sidecar whenever factsOnlyTypes is non-empty, but this writes one only when it has entries. Coana's fix commands don't require the sidecar today, so nothing breaks yet. Still, one helper returning the flags and the sidecar together would stop the two from drifting further.

Expand Down
3 changes: 2 additions & 1 deletion 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,7 +76,7 @@ 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',
Expand Down
3 changes: 3 additions & 0 deletions src/commands/fix/handle-fix.mts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export async function handleFix({
disableExternalToolChecks,
disableMajorUpdates,
dynamicSbomInference,
dynamicSbomInferenceEcosystems,
ecosystems,
exclude,
excludePaths,
Expand Down Expand Up @@ -100,6 +101,7 @@ export async function handleFix({
disableExternalToolChecks,
disableMajorUpdates,
dynamicSbomInference,
dynamicSbomInferenceEcosystems,
ecosystems,
exclude,
excludePaths,
Expand Down Expand Up @@ -130,6 +132,7 @@ export async function handleFix({
disableExternalToolChecks,
disableMajorUpdates,
dynamicSbomInference,
dynamicSbomInferenceEcosystems,
ecosystems,
exclude,
excludePaths,
Expand Down
1 change: 1 addition & 0 deletions src/commands/fix/types.mts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export type FixConfig = {
disableExternalToolChecks: boolean
disableMajorUpdates: boolean
dynamicSbomInference: boolean
dynamicSbomInferenceEcosystems: readonly PURL_Type[]
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
20 changes: 20 additions & 0 deletions src/commands/manifest/discover-manifest-roots.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,26 @@ describe('findBuildToolCandidates', () => {
expect(candidates.get('gradle')?.length).toBeGreaterThan(0)
})

it('drops the build tools of unselected purl types', async () => {
const maven = await findBuildToolCandidates({
cwd: monorepo,
purlTypes: ['maven'],
socketConfig: undefined,
sockJson: {} as SocketJson,
})
expect(maven.get('maven')?.length).toBeGreaterThan(0)
expect(maven.get('gradle')?.length).toBeGreaterThan(0)
expect(maven.has('dotnet')).toBe(false)

const nuget = await findBuildToolCandidates({
cwd: monorepo,
purlTypes: ['nuget'],
socketConfig: undefined,
sockJson: {} as SocketJson,
})
expect([...nuget.keys()]).toEqual(['dotnet'])
})

it('respects --exclude-paths', async () => {
const candidates = await findBuildToolCandidates({
cwd: monorepo,
Expand Down
4 changes: 4 additions & 0 deletions src/commands/manifest/generate-recursive-manifests.mts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import type {
BuildOptionHints,
} from './scripts/resolution-report-render.mts'
import type { SidecarAccumulator } from './scripts/sidecar.mts'
import type { PURL_Type } from '../../utils/ecosystem.mts'
import type { SocketJson } from '../../utils/socket-json.mts'

export type RecursiveManifestOutcomeStatus =
Expand Down Expand Up @@ -362,6 +363,7 @@ export async function generateRecursiveManifests({
excludePaths,
ignoreUnresolved,
optionFlags = new Set(),
purlTypes,
sbtTmpDir,
sidecarAcc,
verbose,
Expand All @@ -374,6 +376,7 @@ export async function generateRecursiveManifests({
// Options the invoking command takes as flags; hints send the rest to each
// build root's socket.json.
optionFlags?: ReadonlySet<BuildOption> | undefined
purlTypes?: readonly PURL_Type[] | undefined
// Reachability path only: caller-supplied directory to use as sbt's shared
// global base across every sbt root in this run. sbt provisions the Scala
// toolchain under `<base>/boot`, which withFiles' artifactPaths point into,
Expand Down Expand Up @@ -401,6 +404,7 @@ export async function generateRecursiveManifests({
const candidatesByTool = await findBuildToolCandidates({
cwd,
excludePaths,
purlTypes,
socketConfig: loadSocketConfig(cwd),
sockJson: withoutDisabledFlags(rootSockJson),
})
Expand Down
15 changes: 15 additions & 0 deletions src/commands/manifest/scripts/build-tool.mts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import type { PURL_Type } from '../../../utils/ecosystem.mts'

export type BuildTool = 'dotnet' | 'gradle' | 'maven' | 'sbt'

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

// The purl type of the packages each tool resolves; exhaustive, so a new tool
// cannot silently produce another ecosystem's purls.
export const PURL_TYPE_BY_TOOL: Record<BuildTool, PURL_Type> = {
Expand All @@ -17,6 +24,14 @@ export const PURL_TYPE_BY_TOOL: Record<BuildTool, PURL_Type> = {
sbt: 'maven',
} as unknown as Record<BuildTool, PURL_Type>

export const FACTS_PURL_TYPES: readonly PURL_Type[] = [
...new Set(Object.values(PURL_TYPE_BY_TOOL)),
].sort()

export function buildToolsFor(purlTypes: readonly PURL_Type[]): BuildTool[] {
return BUILD_TOOLS.filter(tool => purlTypes.includes(PURL_TYPE_BY_TOOL[tool]))
}

// Purl types whose packages have no namespace (NuGet ids are flat).
export const NAMESPACELESS_PURL_TYPES: ReadonlySet<string> = new Set(['nuget'])

Expand Down
18 changes: 18 additions & 0 deletions src/commands/scan/cmd-scan-create.mts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ import { outputCreateNewScan } from './output-create-new-scan.mts'
import { resolveReachAdvisories } from './reach-advisories.mts'
import {
DYNAMIC_SBOM_INFERENCE_DESCRIPTION,
dynamicSbomInferenceEcosystemsFlags,
excludePathsFlag,
parseDynamicSbomInferenceEcosystems,
reachabilityFlags,
} from './reachability-flags.mts'
import {
Expand Down Expand Up @@ -115,6 +117,7 @@ const generalFlags: MeowFlags = {
default: false,
description: `${DYNAMIC_SBOM_INFERENCE_DESCRIPTION} Combine with --reach to split the reachability analysis per project/module.`,
},
...dynamicSbomInferenceEcosystemsFlags,
interactive: {
type: 'boolean',
default: true,
Expand Down Expand Up @@ -355,6 +358,13 @@ async function run(
cmdFlagValueToArray(cli.flags['reachEcosystems']),
)

const dynamicSbomInferenceEcosystemsRaw = cmdFlagValueToArray(
cli.flags['dynamicSbomInferenceEcosystems'],
)
const dynamicSbomInferenceEcosystems = parseDynamicSbomInferenceEcosystems(
dynamicSbomInferenceEcosystemsRaw,
)

warnSocketJsonOnlyFlags(cli.flags, `${parentName} ${CMD_NAME}`)

const dryRun = !!cli.flags['dryRun']
Expand Down Expand Up @@ -587,6 +597,13 @@ async function run(

const wasValidInput = checkCommandInput(
outputKind,
{
nook: true,
test: dynamicSbomInference || !dynamicSbomInferenceEcosystemsRaw.length,
message:
'--dynamic-sbom-inference-ecosystems requires --dynamic-sbom-inference',
fail: 'add --dynamic-sbom-inference',
},
{
nook: true,
test: !!orgSlug,
Expand Down Expand Up @@ -694,6 +711,7 @@ async function run(
pullRequest: Number(pullRequest),
reach: {
dynamicSbomInference: Boolean(dynamicSbomInference),
dynamicSbomInferenceEcosystems,
excludePaths,
reachAdvisories,
reachAnalysisMemoryLimit,
Expand Down
Loading
Loading