Skip to content

Commit 80ccc51

Browse files
authored
feat(scan): forward socket.json build-tool config into reachability (1.1.120, Coana 15.4.1) (#1362)
* feat(scan): forward socket.json build-tool config into reachability socket scan create --reach now maps socket.json's per-ecosystem manifest build-tool options (bin, include/exclude-configs, gradle/sbt opts) into a Coana-defined AutoManifestConfig and passes it to `coana run` via --auto-manifest-config (a temp JSON file path Coana reads), so reach-time dependency resolution invokes the build tool the way the project is configured rather than with defaults. Under --auto-manifest the config also carries top-level failOnBuildToolError=true (fail-closed: Coana treats a build-tool step failure as fatal instead of tolerating it); plain --reach leaves it unset and stays permissive. This is the socket-cli side of the manifest-flag-propagation gap. The Coana `--auto-manifest-config` option is not yet released, so this must not ship until Coana publishes it and the pinned @coana-tech/cli is bumped; until then it is exercised via SOCKET_CLI_COANA_LOCAL_PATH. - add src/utils/auto-manifest-config.mts: BuildToolOptions/AutoManifestConfig types + buildAutoManifestConfig (socket.json -> config) + tests - ReachabilityOptions.autoManifestConfig; write the config to a temp file and pass its path to coana run, cleaning it up after - build the config at the cmd-scan-create assembly point * chore(release): 1.1.120 — Coana 15.4.1 and socket.json build-tool config forwarding Bump @coana-tech/cli to 15.4.1 (which ships the --auto-manifest-config option the feat commit depends on), bump the package version to 1.1.120, and add the changelog entry. * fix(scan): gate --auto-manifest-config on Coana version support Only forward the socket.json build-tool config to `coana run` when the resolved Coana version supports `--auto-manifest-config` (>= 15.4.1). Passing the flag to an older Coana, pinned via --reach-version, would abort the run on an unknown flag; we now skip it and warn instead. A local Coana build (SOCKET_CLI_COANA_LOCAL_PATH) has no resolvable version and is treated as supported. Also drop internal tracker references from source comments. * fix(scan): drop Coana version gate, clean up config temp file in finally Coana is pinned to a specific version in the CLI, so gating --auto-manifest-config on a minimum version added no real safety; remove coanaSupportsAutoManifestConfig and forward the config unconditionally when non-empty. Move the temp config file cleanup into a finally block so it runs even if the Coana run throws. * fix changelog entry * bump coana to 15.4.5
1 parent 0e41460 commit 80ccc51

8 files changed

Lines changed: 309 additions & 47 deletions

CHANGELOG.md

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@ All notable changes to this project will be documented in this file.
44

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

7+
## [1.1.120](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.120) - 2026-06-12
8+
9+
### Changed
10+
- `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.
11+
- `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.
12+
- Updated the Coana CLI to v `15.4.5`.
13+
714
## [1.1.119](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.119) - 2026-06-11
815

916
### Changed
@@ -101,7 +108,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
101108
### Changed
102109
- Updated the Coana CLI to v `15.3.9`.
103110

104-
## [1.1.98](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.98) - 2026-05-22
111+
## [1.1.102](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.98) - 2026-05-22
105112

106113
### Added
107114
- **`socket manifest gradle --facts [beta]`** (and its `socket manifest kotlin --facts` alias) — Emit a `.socket.facts.json` dependency graph from a Gradle build for `socket scan create` to consume as a pregenerated SBOM. Toggle also exposed via the `socket manifest setup` wizard for use with `--auto-manifest`.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "socket",
3-
"version": "1.1.119",
3+
"version": "1.1.120",
44
"description": "CLI for Socket.dev",
55
"homepage": "https://github.com/SocketDev/socket-cli",
66
"license": "MIT",
@@ -96,7 +96,7 @@
9696
"@babel/preset-typescript": "7.27.1",
9797
"@babel/runtime": "7.28.4",
9898
"@biomejs/biome": "2.2.4",
99-
"@coana-tech/cli": "15.3.26",
99+
"@coana-tech/cli": "15.4.5",
100100
"@cyclonedx/cdxgen": "12.1.2",
101101
"@dotenvx/dotenvx": "1.49.0",
102102
"@eslint/compat": "1.3.2",

pnpm-lock.yaml

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/commands/ci/fetch-default-org-slug.test.mts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { beforeEach, describe, expect, it, vi } from 'vitest'
22

33
import { getDefaultOrgSlug } from './fetch-default-org-slug.mts'
4-
import { fetchOrganization } from '../organization/fetch-organization-list.mts'
54
import { getConfigValueOrUndef } from '../../utils/config.mts'
5+
import { fetchOrganization } from '../organization/fetch-organization-list.mts'
66

77
vi.mock('../organization/fetch-organization-list.mts', () => ({
88
fetchOrganization: vi.fn(),

src/commands/scan/cmd-scan-create.mts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { suggestTarget } from './suggest_target.mts'
1313
import { validateReachabilityTarget } from './validate-reachability-target.mts'
1414
import constants, { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts'
1515
import { commonFlags, outputFlags } from '../../flags.mts'
16+
import { buildAutoManifestConfig } from '../../utils/auto-manifest-config.mts'
1617
import { checkCommandInput } from '../../utils/check-input.mts'
1718
import { cmdFlagValueToArray } from '../../utils/cmd.mts'
1819
import { determineOrgSlug } from '../../utils/determine-org-slug.mts'
@@ -622,6 +623,15 @@ async function run(
622623
pendingHead: Boolean(pendingHead),
623624
pullRequest: Number(pullRequest),
624625
reach: {
626+
// Build-tool config for the reach-time resolution, mapped from socket.json
627+
// (per-ecosystem). Best-effort on plain --reach; under --auto-manifest the
628+
// config carries top-level failOnBuildToolError=true (fail-closed). Only
629+
// built when reachability runs.
630+
autoManifestConfig: reach
631+
? buildAutoManifestConfig(sockJson, {
632+
autoManifest: Boolean(autoManifest),
633+
})
634+
: undefined,
625635
excludePaths,
626636
reachAnalysisMemoryLimit: Number(reachAnalysisMemoryLimit),
627637
reachAnalysisTimeout: Number(reachAnalysisTimeout),

src/commands/scan/perform-reachability-analysis.mts

Lines changed: 78 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
1+
import { randomUUID } from 'node:crypto'
2+
import { promises as fs } from 'node:fs'
3+
import { tmpdir } from 'node:os'
14
import path from 'node:path'
25

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

58
import constants from '../../constants.mts'
69
import { handleApiCall } from '../../utils/api.mts'
10+
import { isAutoManifestConfigEmpty } from '../../utils/auto-manifest-config.mts'
711
import { extractTier1ReachabilityScanId } from '../../utils/coana.mts'
812
import { spawnCoanaDlx } from '../../utils/dlx.mts'
913
import { hasEnterpriseOrgPlan } from '../../utils/organization.mts'
@@ -12,10 +16,12 @@ import { socketDevLink } from '../../utils/terminal-link.mts'
1216
import { fetchOrganization } from '../organization/fetch-organization-list.mts'
1317

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

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

172178
const outputFilePath = outputPath || constants.DOT_SOCKET_DOT_FACTS_JSON
179+
180+
// Coana reads `--auto-manifest-config` from a JSON file, so write the resolved
181+
// per-ecosystem build-tool config (mapped from socket.json) to a temp file and
182+
// pass its absolute path. Cleaned up in the finally below.
183+
let autoManifestConfigPath: string | undefined
184+
const { autoManifestConfig } = reachabilityOptions
185+
if (autoManifestConfig && !isAutoManifestConfigEmpty(autoManifestConfig)) {
186+
autoManifestConfigPath = path.join(
187+
tmpdir(),
188+
`socket-auto-manifest-config-${randomUUID()}.json`,
189+
)
190+
await fs.writeFile(
191+
autoManifestConfigPath,
192+
JSON.stringify(autoManifestConfig),
193+
'utf8',
194+
)
195+
}
196+
173197
// Build Coana arguments.
174198
const coanaArgs = [
175199
'run',
@@ -228,6 +252,11 @@ export async function performReachabilityAnalysis(
228252
...(reachabilityOptions.reachUseOnlyPregeneratedSboms
229253
? ['--use-only-pregenerated-sboms']
230254
: []),
255+
// Hand the per-ecosystem build-tool config (mapped from socket.json) to
256+
// Coana's reach-time resolution, as a temp JSON file path.
257+
...(autoManifestConfigPath
258+
? ['--auto-manifest-config', autoManifestConfigPath]
259+
: []),
231260
]
232261

233262
// Build environment variables.
@@ -241,48 +270,59 @@ export async function performReachabilityAnalysis(
241270
coanaEnv['SOCKET_BRANCH_NAME'] = branchName
242271
}
243272

244-
// Run Coana with the manifests tar hash.
245-
const coanaResult = await spawnCoanaDlx(coanaArgs, orgSlug, {
246-
coanaVersion: reachabilityOptions.reachVersion,
247-
cwd,
248-
env: coanaEnv,
249-
spinner,
250-
stdio: 'inherit',
251-
})
273+
try {
274+
// Run Coana with the manifests tar hash.
275+
const coanaResult = await spawnCoanaDlx(coanaArgs, orgSlug, {
276+
coanaVersion: reachabilityOptions.reachVersion,
277+
cwd,
278+
env: coanaEnv,
279+
spinner,
280+
stdio: 'inherit',
281+
})
252282

253-
if (wasSpinning) {
254-
spinner.start()
255-
}
283+
if (wasSpinning) {
284+
spinner.start()
285+
}
256286

257-
if (!coanaResult.ok) {
258-
const coanaVersion =
259-
reachabilityOptions.reachVersion ||
260-
constants.ENV.INLINED_SOCKET_CLI_COANA_TECH_CLI_VERSION
261-
logger.error(
262-
`Coana reachability analysis failed. Version: ${coanaVersion}, target: ${analysisTarget}, cwd: ${cwd}`,
263-
)
264-
if (coanaResult.message) {
265-
logger.error(`Details: ${coanaResult.message}`)
287+
if (!coanaResult.ok) {
288+
const coanaVersion =
289+
reachabilityOptions.reachVersion ||
290+
constants.ENV.INLINED_SOCKET_CLI_COANA_TECH_CLI_VERSION
291+
logger.error(
292+
`Coana reachability analysis failed. Version: ${coanaVersion}, target: ${analysisTarget}, cwd: ${cwd}`,
293+
)
294+
if (coanaResult.message) {
295+
logger.error(`Details: ${coanaResult.message}`)
296+
}
297+
return coanaResult
266298
}
267-
return coanaResult
268-
}
269299

270-
// Coana writes the facts file relative to the scan `cwd` (it is spawned
271-
// with `cwd` above), so resolve the read path against `cwd` too. Reading
272-
// the bare relative path would resolve against `process.cwd()` and miss
273-
// the file whenever `cwd !== process.cwd()` (e.g. `--cwd <dir>`), silently
274-
// dropping the tier 1 scan id and skipping finalize downstream.
275-
const resolvedReportPath = path.resolve(cwd, outputFilePath)
300+
// Coana writes the facts file relative to the scan `cwd` (it is spawned
301+
// with `cwd` above), so resolve the read path against `cwd` too. Reading
302+
// the bare relative path would resolve against `process.cwd()` and miss
303+
// the file whenever `cwd !== process.cwd()` (e.g. `--cwd <dir>`), silently
304+
// dropping the tier 1 scan id and skipping finalize downstream.
305+
const resolvedReportPath = path.resolve(cwd, outputFilePath)
276306

277-
return {
278-
ok: true,
279-
data: {
280-
// Use the actual output filename for the scan. Keep this `cwd`-relative
281-
// so the upload (which relativizes against `cwd`) and the post-success
282-
// unlink (`path.resolve(cwd, reachabilityReport)`) keep working.
283-
reachabilityReport: outputFilePath,
284-
tier1ReachabilityScanId:
285-
extractTier1ReachabilityScanId(resolvedReportPath),
286-
},
307+
return {
308+
ok: true,
309+
data: {
310+
// Use the actual output filename for the scan. Keep this `cwd`-relative
311+
// so the upload (which relativizes against `cwd`) and the post-success
312+
// unlink (`path.resolve(cwd, reachabilityReport)`) keep working.
313+
reachabilityReport: outputFilePath,
314+
tier1ReachabilityScanId:
315+
extractTier1ReachabilityScanId(resolvedReportPath),
316+
},
317+
}
318+
} finally {
319+
// The run no longer needs the temp config file; best-effort cleanup.
320+
if (autoManifestConfigPath) {
321+
try {
322+
await fs.unlink(autoManifestConfigPath)
323+
} catch {
324+
// File may already be gone or unwritable.
325+
}
326+
}
287327
}
288328
}

src/utils/auto-manifest-config.mts

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
import type { SocketJson } from './socket-json.mts'
2+
3+
// Per-ecosystem build-tool options handed off to the Coana CLI — used both when
4+
// generating manifests (`coana manifest <ecosystem>`) and, in socket mode, for
5+
// reach-time dependency resolution (`coana run`). This mirrors the Coana-side
6+
// `--auto-manifest-config` shape: socket-cli owns mapping `socket.json` onto it,
7+
// so Coana stays uncoupled from `socket.json`'s schema. Keeping the
8+
// per-ecosystem options namespaced (rather than as flat CLI flags) avoids the
9+
// ambiguity of a bare `--bin`/`--include-configs` when a repo has more than one
10+
// build tool.
11+
export type BuildToolOptions = {
12+
// Build-tool executable override (e.g. `./gradlew`, `atlas-mvn`).
13+
bin?: string | undefined
14+
// Comma-separated config-name globs to skip.
15+
excludeConfigs?: string | undefined
16+
// `socket.json`'s per-ecosystem `ignoreUnresolved` (warn vs fail on unresolved
17+
// dependencies), forwarded verbatim. NOTE: this is NOT the reach-time
18+
// fail-closed switch — that's the run-wide `failOnBuildToolError` below.
19+
ignoreUnresolved?: boolean | undefined
20+
// Comma-separated config-name globs to resolve.
21+
includeConfigs?: string | undefined
22+
// Extra build-tool options, pre-split into argv. Coana maps these straight to
23+
// the tool's opts (no splitting on its side). Mapped from `socket.json`'s
24+
// `gradleOpts`/`sbtOpts` string.
25+
opts?: string[] | undefined
26+
}
27+
28+
// The Coana hand-off config. `failOnBuildToolError` is run-wide (top level)
29+
// because `--auto-manifest` is a single CLI mode, not a per-package-manager
30+
// setting. The per-ecosystem entries are present only for ecosystems configured
31+
// (and not disabled) in `socket.json`; absent ecosystems fall to Coana's own
32+
// defaults.
33+
export type AutoManifestConfig = {
34+
// Run-wide fail-closed switch. When true, Coana treats a build-tool step
35+
// failure as fatal rather than tolerating it. socket-cli sets it true under
36+
// `--auto-manifest`; left unset on plain `--reach` (permissive — Coana's
37+
// default best-effort behaviour).
38+
failOnBuildToolError?: boolean | undefined
39+
gradle?: BuildToolOptions | undefined
40+
sbt?: BuildToolOptions | undefined
41+
}
42+
43+
// Splits a `socket.json` opts string (`gradleOpts`/`sbtOpts`) into argv, matching
44+
// how the standalone `socket manifest` path splits it. Returns undefined when
45+
// there's nothing to pass so the field is omitted from the config.
46+
function parseOpts(value: string | undefined): string[] | undefined {
47+
if (!value) {
48+
return undefined
49+
}
50+
const parts = value
51+
.split(' ')
52+
.map(s => s.trim())
53+
.filter(Boolean)
54+
return parts.length ? parts : undefined
55+
}
56+
57+
// Maps `socket.json`'s `defaults.manifest.<ecosystem>` build-tool options onto
58+
// the Coana hand-off config.
59+
//
60+
// `autoManifest` reflects whether the run is `--auto-manifest` (fail-closed:
61+
// `failOnBuildToolError=true`) vs plain `--reach` (permissive:
62+
// `failOnBuildToolError` left unset so Coana's default applies). Per-ecosystem
63+
// options are forwarded verbatim from `socket.json`; disabled ecosystems are
64+
// omitted so they fall back to Coana's defaults.
65+
export function buildAutoManifestConfig(
66+
sockJson: SocketJson,
67+
{ autoManifest }: { autoManifest: boolean },
68+
): AutoManifestConfig {
69+
const manifest = sockJson.defaults?.manifest
70+
const config: AutoManifestConfig = {}
71+
72+
// `--auto-manifest` expects every build-tool command to succeed, so a
73+
// build-tool step failure should be fatal rather than tolerated.
74+
if (autoManifest) {
75+
config.failOnBuildToolError = true
76+
}
77+
78+
const gradle = manifest?.gradle
79+
if (gradle && !gradle.disabled) {
80+
config.gradle = {
81+
bin: gradle.bin,
82+
excludeConfigs: gradle.excludeConfigs,
83+
ignoreUnresolved: gradle.ignoreUnresolved,
84+
includeConfigs: gradle.includeConfigs,
85+
opts: parseOpts(gradle.gradleOpts),
86+
}
87+
}
88+
89+
const sbt = manifest?.sbt
90+
if (sbt && !sbt.disabled) {
91+
config.sbt = {
92+
bin: sbt.bin,
93+
excludeConfigs: sbt.excludeConfigs,
94+
ignoreUnresolved: sbt.ignoreUnresolved,
95+
includeConfigs: sbt.includeConfigs,
96+
opts: parseOpts(sbt.sbtOpts),
97+
}
98+
}
99+
100+
return config
101+
}
102+
103+
// True when there's nothing to hand to Coana: no per-ecosystem options and the
104+
// run mode is left at Coana's permissive default. When true, the
105+
// `--auto-manifest-config` option should be omitted entirely.
106+
export function isAutoManifestConfigEmpty(config: AutoManifestConfig): boolean {
107+
return (
108+
!config.gradle && !config.sbt && config.failOnBuildToolError === undefined
109+
)
110+
}

0 commit comments

Comments
 (0)