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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

## [1.1.120](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.120) - 2026-06-12

### Changed
- `socket scan create --reach` now applies your project's build-tool settings from `socket.json` (configured via `socket manifest setup`) — custom build-tool binary, include/exclude configs, and Gradle/sbt options — when resolving dependencies for Gradle and sbt reachability analysis, instead of always invoking the build tool with defaults.
- `socket scan create --auto-manifest --reach` now fails with an error when a build tool fails during manifest generation, rather than tolerating it. Plain `--reach` (without `--auto-manifest`) keeps generating manifests on a best-effort basis.
- Updated the Coana CLI to v `15.4.1`.

## [1.1.119](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.119) - 2026-06-11

### Changed
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "socket",
"version": "1.1.119",
"version": "1.1.120",
"description": "CLI for Socket.dev",
"homepage": "https://github.com/SocketDev/socket-cli",
"license": "MIT",
Expand Down Expand Up @@ -96,7 +96,7 @@
"@babel/preset-typescript": "7.27.1",
"@babel/runtime": "7.28.4",
"@biomejs/biome": "2.2.4",
"@coana-tech/cli": "15.3.26",
"@coana-tech/cli": "15.4.1",
"@cyclonedx/cdxgen": "12.1.2",
"@dotenvx/dotenvx": "1.49.0",
"@eslint/compat": "1.3.2",
Expand Down
10 changes: 5 additions & 5 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 10 additions & 0 deletions src/commands/scan/cmd-scan-create.mts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { suggestTarget } from './suggest_target.mts'
import { validateReachabilityTarget } from './validate-reachability-target.mts'
import constants, { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts'
import { commonFlags, outputFlags } from '../../flags.mts'
import { buildAutoManifestConfig } from '../../utils/auto-manifest-config.mts'
import { checkCommandInput } from '../../utils/check-input.mts'
import { cmdFlagValueToArray } from '../../utils/cmd.mts'
import { determineOrgSlug } from '../../utils/determine-org-slug.mts'
Expand Down Expand Up @@ -622,6 +623,15 @@ async function run(
pendingHead: Boolean(pendingHead),
pullRequest: Number(pullRequest),
reach: {
// Build-tool config for the reach-time resolution, mapped from socket.json
// (per-ecosystem). Best-effort on plain --reach; under --auto-manifest the
// config carries top-level failOnBuildToolError=true (fail-closed). Only
// built when reachability runs.
autoManifestConfig: reach
? buildAutoManifestConfig(sockJson, {
autoManifest: Boolean(autoManifest),
})
: undefined,
excludePaths,
reachAnalysisMemoryLimit: Number(reachAnalysisMemoryLimit),
reachAnalysisTimeout: Number(reachAnalysisTimeout),
Expand Down
39 changes: 39 additions & 0 deletions src/commands/scan/perform-reachability-analysis.mts
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import { randomUUID } from 'node:crypto'
import { promises as fs } from 'node:fs'
import { tmpdir } from 'node:os'
import path from 'node:path'

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

import constants from '../../constants.mts'
import { handleApiCall } from '../../utils/api.mts'
import { isAutoManifestConfigEmpty } from '../../utils/auto-manifest-config.mts'
import { extractTier1ReachabilityScanId } from '../../utils/coana.mts'
import { spawnCoanaDlx } from '../../utils/dlx.mts'
import { hasEnterpriseOrgPlan } from '../../utils/organization.mts'
Expand All @@ -12,10 +16,12 @@ import { socketDevLink } from '../../utils/terminal-link.mts'
import { fetchOrganization } from '../organization/fetch-organization-list.mts'

import type { CResult } from '../../types.mts'
import type { AutoManifestConfig } from '../../utils/auto-manifest-config.mts'
import type { PURL_Type } from '../../utils/ecosystem.mts'
import type { Spinner } from '@socketsecurity/registry/lib/spinner'

export type ReachabilityOptions = {
autoManifestConfig?: AutoManifestConfig | undefined
excludePaths: string[]
reachAnalysisMemoryLimit: number
reachAnalysisTimeout: number
Expand Down Expand Up @@ -170,6 +176,24 @@ export async function performReachabilityAnalysis(
spinner?.infoAndStop('Running reachability analysis with Coana...')

const outputFilePath = outputPath || constants.DOT_SOCKET_DOT_FACTS_JSON

// Coana reads `--auto-manifest-config` from a JSON file, so write the resolved
// per-ecosystem build-tool config (mapped from socket.json) to a temp file and
// pass its absolute path. Cleaned up right after the run below.
let autoManifestConfigPath: string | undefined
const { autoManifestConfig } = reachabilityOptions
if (autoManifestConfig && !isAutoManifestConfigEmpty(autoManifestConfig)) {
autoManifestConfigPath = path.join(
tmpdir(),
`socket-auto-manifest-config-${randomUUID()}.json`,
)
await fs.writeFile(
autoManifestConfigPath,
JSON.stringify(autoManifestConfig),
'utf8',
)
}

// Build Coana arguments.
const coanaArgs = [
'run',
Expand Down Expand Up @@ -228,6 +252,12 @@ export async function performReachabilityAnalysis(
...(reachabilityOptions.reachUseOnlyPregeneratedSboms
? ['--use-only-pregenerated-sboms']
: []),
// Hand the per-ecosystem build-tool config (mapped from socket.json) to
// Coana's reach-time resolution, as a temp JSON file path. Coana side:
// REA-547.
...(autoManifestConfigPath
? ['--auto-manifest-config', autoManifestConfigPath]
: []),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Config flag needs Coana version

Medium Severity

Reachability now appends --auto-manifest-config whenever the mapped config is non-empty, but reachVersion (or a local Coana path) can still invoke a Coana build older than 15.4.1 that does not implement that flag, causing reach analysis to fail unexpectedly.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 478fb22. Configure here.

]

// Build environment variables.
Expand All @@ -250,6 +280,15 @@ export async function performReachabilityAnalysis(
stdio: 'inherit',
})

// The run no longer needs the temp config file; best-effort cleanup.
if (autoManifestConfigPath) {
try {
await fs.unlink(autoManifestConfigPath)
} catch {
// File may already be gone or unwritable.
}
}

if (wasSpinning) {
spinner.start()
}
Expand Down
110 changes: 110 additions & 0 deletions src/utils/auto-manifest-config.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import type { SocketJson } from './socket-json.mts'

// Per-ecosystem build-tool options handed off to the Coana CLI — used both when
// generating manifests (`coana manifest <ecosystem>`) and, in socket mode, for
// reach-time dependency resolution (`coana run`). This mirrors the Coana-side
// `--auto-manifest-config` shape (REA-547): socket-cli owns mapping `socket.json`

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Internal ticket ID in comments

Low Severity

New comments reference the internal tracker id REA-547. Source comments in this repo should describe intent without Jira-style ticket references so they stay readable outside internal tooling.

Additional Locations (1)
Fix in Cursor Fix in Web

Triggered by learned rule: No internal ticket references in code comments — remove Jira/tracker IDs

Reviewed by Cursor Bugbot for commit 478fb22. Configure here.

// onto it, so Coana stays uncoupled from `socket.json`'s schema. Keeping the
// per-ecosystem options namespaced (rather than as flat CLI flags) avoids the
// ambiguity of a bare `--bin`/`--include-configs` when a repo has more than one
// build tool.
export type BuildToolOptions = {
// Build-tool executable override (e.g. `./gradlew`, `atlas-mvn`).
bin?: string | undefined
// Comma-separated config-name globs to skip.
excludeConfigs?: string | undefined
// `socket.json`'s per-ecosystem `ignoreUnresolved` (warn vs fail on unresolved
// dependencies), forwarded verbatim. NOTE: this is NOT the reach-time
// fail-closed switch — that's the run-wide `failOnBuildToolError` below.
ignoreUnresolved?: boolean | undefined
// Comma-separated config-name globs to resolve.
includeConfigs?: string | undefined
// Extra build-tool options, pre-split into argv. Coana maps these straight to
// the tool's opts (no splitting on its side). Mapped from `socket.json`'s
// `gradleOpts`/`sbtOpts` string.
opts?: string[] | undefined
}

// The Coana hand-off config. `failOnBuildToolError` is run-wide (top level)
// because `--auto-manifest` is a single CLI mode, not a per-package-manager
// setting. The per-ecosystem entries are present only for ecosystems configured
// (and not disabled) in `socket.json`; absent ecosystems fall to Coana's own
// defaults.
export type AutoManifestConfig = {
// Run-wide fail-closed switch. When true, Coana treats a build-tool step
// failure as fatal rather than tolerating it. socket-cli sets it true under
// `--auto-manifest`; left unset on plain `--reach` (permissive — Coana's
// default best-effort behaviour).
failOnBuildToolError?: boolean | undefined
gradle?: BuildToolOptions | undefined
sbt?: BuildToolOptions | undefined
}

// Splits a `socket.json` opts string (`gradleOpts`/`sbtOpts`) into argv, matching
// how the standalone `socket manifest` path splits it. Returns undefined when
// there's nothing to pass so the field is omitted from the config.
function parseOpts(value: string | undefined): string[] | undefined {
if (!value) {
return undefined
}
const parts = value
.split(' ')
.map(s => s.trim())
.filter(Boolean)
return parts.length ? parts : undefined
}

// Maps `socket.json`'s `defaults.manifest.<ecosystem>` build-tool options onto
// the Coana hand-off config.
//
// `autoManifest` reflects whether the run is `--auto-manifest` (fail-closed:
// `failOnBuildToolError=true`) vs plain `--reach` (permissive:
// `failOnBuildToolError` left unset so Coana's default applies). Per-ecosystem
// options are forwarded verbatim from `socket.json`; disabled ecosystems are
// omitted so they fall back to Coana's defaults.
export function buildAutoManifestConfig(
sockJson: SocketJson,
{ autoManifest }: { autoManifest: boolean },
): AutoManifestConfig {
const manifest = sockJson.defaults?.manifest
const config: AutoManifestConfig = {}

// `--auto-manifest` expects every build-tool command to succeed, so a
// build-tool step failure should be fatal rather than tolerated.
if (autoManifest) {
config.failOnBuildToolError = true
}

const gradle = manifest?.gradle
if (gradle && !gradle.disabled) {
config.gradle = {
bin: gradle.bin,
excludeConfigs: gradle.excludeConfigs,
ignoreUnresolved: gradle.ignoreUnresolved,
includeConfigs: gradle.includeConfigs,
opts: parseOpts(gradle.gradleOpts),
}
}

const sbt = manifest?.sbt
if (sbt && !sbt.disabled) {
config.sbt = {
bin: sbt.bin,
excludeConfigs: sbt.excludeConfigs,
ignoreUnresolved: sbt.ignoreUnresolved,
includeConfigs: sbt.includeConfigs,
opts: parseOpts(sbt.sbtOpts),
}
}

return config
}

// True when there's nothing to hand to Coana: no per-ecosystem options and the
// run mode is left at Coana's permissive default. When true, the
// `--auto-manifest-config` option should be omitted entirely.
export function isAutoManifestConfigEmpty(config: AutoManifestConfig): boolean {
return (
!config.gradle && !config.sbt && config.failOnBuildToolError === undefined
)
}
95 changes: 95 additions & 0 deletions src/utils/auto-manifest-config.test.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { describe, expect, it } from 'vitest'

import {
buildAutoManifestConfig,
isAutoManifestConfigEmpty,
} from './auto-manifest-config.mts'

import type { SocketJson } from './socket-json.mts'

// Builds a minimal SocketJson for the mapping under test; only
// `defaults.manifest` is read, so the header/version fields are irrelevant.
function socketJson(
manifest?: NonNullable<NonNullable<SocketJson['defaults']>['manifest']>,
): SocketJson {
return { defaults: { manifest } } as SocketJson
}

describe('buildAutoManifestConfig', () => {
it('returns an empty config for plain --reach with no manifest defaults', () => {
expect(
buildAutoManifestConfig(socketJson(), { autoManifest: false }),
).toEqual({})
})

it('sets top-level failOnBuildToolError=true under --auto-manifest (fail-closed)', () => {
expect(
buildAutoManifestConfig(socketJson(), { autoManifest: true }),
).toEqual({ failOnBuildToolError: true })
})

it('leaves failOnBuildToolError unset on plain --reach (Coana default permissive)', () => {
const config = buildAutoManifestConfig(
socketJson({ gradle: { bin: './gradlew' } }),
{ autoManifest: false },
)
expect(config.failOnBuildToolError).toBeUndefined()
})

it('maps gradle/sbt options, *Opts -> opts, ignoreUnresolved passthrough', () => {
const config = buildAutoManifestConfig(
socketJson({
gradle: {
bin: './gradlew',
excludeConfigs: 'testCompileClasspath',
gradleOpts: '--offline --no-daemon',
ignoreUnresolved: true,
includeConfigs: '*RuntimeClasspath',
},
sbt: { bin: 'sbt', sbtOpts: '-batch' },
}),
{ autoManifest: true },
)
expect(config).toEqual({
failOnBuildToolError: true,
gradle: {
bin: './gradlew',
excludeConfigs: 'testCompileClasspath',
ignoreUnresolved: true,
includeConfigs: '*RuntimeClasspath',
opts: ['--offline', '--no-daemon'],
},
sbt: { bin: 'sbt', opts: ['-batch'] },
})
})

it('omits disabled ecosystems so they fall back to Coana defaults', () => {
const config = buildAutoManifestConfig(
socketJson({
gradle: { disabled: true, includeConfigs: '*RuntimeClasspath' },
sbt: { bin: 'sbt' },
}),
{ autoManifest: false },
)
expect(config.gradle).toBeUndefined()
expect(config.sbt).toBeDefined()
})
})

describe('isAutoManifestConfigEmpty', () => {
it('is true when there are no ecosystems and the run mode is default', () => {
expect(isAutoManifestConfigEmpty({})).toBe(true)
})

it('is false when failOnBuildToolError is set (fail-closed must reach Coana)', () => {
expect(isAutoManifestConfigEmpty({ failOnBuildToolError: true })).toBe(
false,
)
})

it('is false when an ecosystem is configured', () => {
expect(isAutoManifestConfigEmpty({ gradle: { bin: './gradlew' } })).toBe(
false,
)
})
})
Loading