Skip to content

Commit 8b5ffcf

Browse files
authored
fix(scan): isolate --json/--markdown output during reachability analysis (#1371)
Reachability spawned the Coana CLI with stdio: 'inherit', so Coana's progress/log output went to the parent's stdout and corrupted the machine-readable payload. Since 2>/dev/null only drops stderr, `socket scan create --reach --json` (and `socket scan reach --json`) could not be isolated to just the JSON the way a non-reach scan can. In json/markdown output modes, route the Coana child's stdout to the parent's stderr (fd 2) via the stdio array ['inherit', 2, 'inherit'] so the final JSON/markdown stays alone on stdout while progress stays visible on stderr. Text mode keeps inheriting stdout unchanged.
1 parent c78c6c6 commit 8b5ffcf

4 files changed

Lines changed: 89 additions & 2 deletions

File tree

src/commands/scan/handle-create-new-scan.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ export async function handleCreateNewScan({
238238
branchName,
239239
cwd,
240240
orgSlug,
241+
outputKind,
241242
packagePaths,
242243
reachabilityOptions: mergedReachabilityOptions,
243244
repoName,

src/commands/scan/handle-scan-reach.mts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ export async function handleScanReach({
9595
const result = await performReachabilityAnalysis({
9696
cwd,
9797
orgSlug,
98+
outputKind,
9899
outputPath,
99100
packagePaths,
100101
reachabilityOptions: mergedReachabilityOptions,

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

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,11 @@ import { setupSdk } from '../../utils/sdk.mts'
1515
import { socketDevLink } from '../../utils/terminal-link.mts'
1616
import { fetchOrganization } from '../organization/fetch-organization-list.mts'
1717

18-
import type { CResult } from '../../types.mts'
18+
import type { CResult, OutputKind } from '../../types.mts'
1919
import type { AutoManifestConfig } from '../../utils/auto-manifest-config.mts'
2020
import type { PURL_Type } from '../../utils/ecosystem.mts'
2121
import type { Spinner } from '@socketsecurity/registry/lib/spinner'
22+
import type { StdioOptions } from 'node:child_process'
2223

2324
export type ReachabilityOptions = {
2425
autoManifestConfig?: AutoManifestConfig | undefined
@@ -47,6 +48,7 @@ export type ReachabilityAnalysisOptions = {
4748
branchName?: string | undefined
4849
cwd?: string | undefined
4950
orgSlug?: string | undefined
51+
outputKind?: OutputKind | undefined
5052
outputPath?: string | undefined
5153
packagePaths?: string[] | undefined
5254
reachabilityOptions: ReachabilityOptions
@@ -68,6 +70,7 @@ export async function performReachabilityAnalysis(
6870
branchName,
6971
cwd = process.cwd(),
7072
orgSlug,
73+
outputKind = 'text',
7174
outputPath,
7275
packagePaths,
7376
reachabilityOptions,
@@ -270,14 +273,22 @@ export async function performReachabilityAnalysis(
270273
coanaEnv['SOCKET_BRANCH_NAME'] = branchName
271274
}
272275

276+
// In machine-readable modes (--json/--markdown) the final payload is written
277+
// to stdout by the output layer. Coana streams progress/logs over stdout
278+
// under `inherit`, which would corrupt that payload, so redirect the child's
279+
// stdout to our stderr (fd 2). Progress stays visible for humans and
280+
// `2>/dev/null` isolates the JSON/markdown. stdin and stderr stay inherited.
281+
const coanaStdio: StdioOptions =
282+
outputKind === 'text' ? 'inherit' : ['inherit', 2, 'inherit']
283+
273284
try {
274285
// Run Coana with the manifests tar hash.
275286
const coanaResult = await spawnCoanaDlx(coanaArgs, orgSlug, {
276287
coanaVersion: reachabilityOptions.reachVersion,
277288
cwd,
278289
env: coanaEnv,
279290
spinner,
280-
stdio: 'inherit',
291+
stdio: coanaStdio,
281292
})
282293

283294
if (wasSpinning) {

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,77 @@ describe('performReachabilityAnalysis facts-file resolution', () => {
162162
expect(result.ok && result.data.tier1ReachabilityScanId).toBeUndefined()
163163
})
164164
})
165+
166+
describe('performReachabilityAnalysis stdio routing by output kind', () => {
167+
let scanCwd: string
168+
169+
beforeEach(() => {
170+
vi.clearAllMocks()
171+
mockFetchOrganization.mockResolvedValue({
172+
ok: true,
173+
data: { organizations: {} },
174+
})
175+
mockHasEnterpriseOrgPlan.mockReturnValue(true)
176+
mockSpawnCoanaDlx.mockResolvedValue({ ok: true, data: '' })
177+
scanCwd = mkdtempSync(path.join(tmpdir(), 'socket-rea-stdio-'))
178+
writeFileSync(
179+
path.join(scanCwd, '.socket.facts.json'),
180+
JSON.stringify({ components: [] }),
181+
)
182+
})
183+
184+
afterEach(() => {
185+
rmSync(scanCwd, { force: true, recursive: true })
186+
})
187+
188+
it('inherits stdio in text output mode', async () => {
189+
await performReachabilityAnalysis({
190+
cwd: scanCwd,
191+
outputKind: 'text',
192+
reachabilityOptions: makeReachabilityOptions(),
193+
target: scanCwd,
194+
})
195+
196+
expect(mockSpawnCoanaDlx.mock.calls[0]![2]).toMatchObject({
197+
stdio: 'inherit',
198+
})
199+
})
200+
201+
it('defaults to inheriting stdio when no output kind is given', async () => {
202+
await performReachabilityAnalysis({
203+
cwd: scanCwd,
204+
reachabilityOptions: makeReachabilityOptions(),
205+
target: scanCwd,
206+
})
207+
208+
expect(mockSpawnCoanaDlx.mock.calls[0]![2]).toMatchObject({
209+
stdio: 'inherit',
210+
})
211+
})
212+
213+
it('redirects Coana stdout to stderr (fd 2) in json output mode', async () => {
214+
await performReachabilityAnalysis({
215+
cwd: scanCwd,
216+
outputKind: 'json',
217+
reachabilityOptions: makeReachabilityOptions(),
218+
target: scanCwd,
219+
})
220+
221+
expect(mockSpawnCoanaDlx.mock.calls[0]![2]).toMatchObject({
222+
stdio: ['inherit', 2, 'inherit'],
223+
})
224+
})
225+
226+
it('redirects Coana stdout to stderr (fd 2) in markdown output mode', async () => {
227+
await performReachabilityAnalysis({
228+
cwd: scanCwd,
229+
outputKind: 'markdown',
230+
reachabilityOptions: makeReachabilityOptions(),
231+
target: scanCwd,
232+
})
233+
234+
expect(mockSpawnCoanaDlx.mock.calls[0]![2]).toMatchObject({
235+
stdio: ['inherit', 2, 'inherit'],
236+
})
237+
})
238+
})

0 commit comments

Comments
 (0)