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
1 change: 1 addition & 0 deletions src/commands/scan/handle-create-new-scan.mts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ export async function handleCreateNewScan({
branchName,
cwd,
orgSlug,
outputKind,
packagePaths,
reachabilityOptions: mergedReachabilityOptions,
repoName,
Expand Down
1 change: 1 addition & 0 deletions src/commands/scan/handle-scan-reach.mts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export async function handleScanReach({
const result = await performReachabilityAnalysis({
cwd,
orgSlug,
outputKind,
outputPath,
packagePaths,
reachabilityOptions: mergedReachabilityOptions,
Expand Down
15 changes: 13 additions & 2 deletions src/commands/scan/perform-reachability-analysis.mts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,11 @@ import { setupSdk } from '../../utils/sdk.mts'
import { socketDevLink } from '../../utils/terminal-link.mts'
import { fetchOrganization } from '../organization/fetch-organization-list.mts'

import type { CResult } from '../../types.mts'
import type { CResult, OutputKind } 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'
import type { StdioOptions } from 'node:child_process'

export type ReachabilityOptions = {
autoManifestConfig?: AutoManifestConfig | undefined
Expand Down Expand Up @@ -47,6 +48,7 @@ export type ReachabilityAnalysisOptions = {
branchName?: string | undefined
cwd?: string | undefined
orgSlug?: string | undefined
outputKind?: OutputKind | undefined
outputPath?: string | undefined
packagePaths?: string[] | undefined
reachabilityOptions: ReachabilityOptions
Expand All @@ -68,6 +70,7 @@ export async function performReachabilityAnalysis(
branchName,
cwd = process.cwd(),
orgSlug,
outputKind = 'text',
outputPath,
packagePaths,
reachabilityOptions,
Expand Down Expand Up @@ -270,14 +273,22 @@ export async function performReachabilityAnalysis(
coanaEnv['SOCKET_BRANCH_NAME'] = branchName
}

// In machine-readable modes (--json/--markdown) the final payload is written
// to stdout by the output layer. Coana streams progress/logs over stdout
// under `inherit`, which would corrupt that payload, so redirect the child's
// stdout to our stderr (fd 2). Progress stays visible for humans and
// `2>/dev/null` isolates the JSON/markdown. stdin and stderr stay inherited.
const coanaStdio: StdioOptions =
outputKind === 'text' ? 'inherit' : ['inherit', 2, 'inherit']

try {
// Run Coana with the manifests tar hash.
const coanaResult = await spawnCoanaDlx(coanaArgs, orgSlug, {
coanaVersion: reachabilityOptions.reachVersion,
cwd,
env: coanaEnv,
spinner,
stdio: 'inherit',
stdio: coanaStdio,
})

if (wasSpinning) {
Expand Down
74 changes: 74 additions & 0 deletions src/commands/scan/perform-reachability-analysis.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -162,3 +162,77 @@ describe('performReachabilityAnalysis facts-file resolution', () => {
expect(result.ok && result.data.tier1ReachabilityScanId).toBeUndefined()
})
})

describe('performReachabilityAnalysis stdio routing by output kind', () => {
let scanCwd: string

beforeEach(() => {
vi.clearAllMocks()
mockFetchOrganization.mockResolvedValue({
ok: true,
data: { organizations: {} },
})
mockHasEnterpriseOrgPlan.mockReturnValue(true)
mockSpawnCoanaDlx.mockResolvedValue({ ok: true, data: '' })
scanCwd = mkdtempSync(path.join(tmpdir(), 'socket-rea-stdio-'))
writeFileSync(
path.join(scanCwd, '.socket.facts.json'),
JSON.stringify({ components: [] }),
)
})

afterEach(() => {
rmSync(scanCwd, { force: true, recursive: true })
})

it('inherits stdio in text output mode', async () => {
await performReachabilityAnalysis({
cwd: scanCwd,
outputKind: 'text',
reachabilityOptions: makeReachabilityOptions(),
target: scanCwd,
})

expect(mockSpawnCoanaDlx.mock.calls[0]![2]).toMatchObject({
stdio: 'inherit',
})
})

it('defaults to inheriting stdio when no output kind is given', async () => {
await performReachabilityAnalysis({
cwd: scanCwd,
reachabilityOptions: makeReachabilityOptions(),
target: scanCwd,
})

expect(mockSpawnCoanaDlx.mock.calls[0]![2]).toMatchObject({
stdio: 'inherit',
})
})

it('redirects Coana stdout to stderr (fd 2) in json output mode', async () => {
await performReachabilityAnalysis({
cwd: scanCwd,
outputKind: 'json',
reachabilityOptions: makeReachabilityOptions(),
target: scanCwd,
})

expect(mockSpawnCoanaDlx.mock.calls[0]![2]).toMatchObject({
stdio: ['inherit', 2, 'inherit'],
})
})

it('redirects Coana stdout to stderr (fd 2) in markdown output mode', async () => {
await performReachabilityAnalysis({
cwd: scanCwd,
outputKind: 'markdown',
reachabilityOptions: makeReachabilityOptions(),
target: scanCwd,
})

expect(mockSpawnCoanaDlx.mock.calls[0]![2]).toMatchObject({
stdio: ['inherit', 2, 'inherit'],
})
})
})
Loading