Skip to content
Merged
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
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ 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.39](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.39) - 2025-12-01

### Added
- Added the `--output <scan-report.json>` flag to `socket scan reach`.

### Changed
- Updated the Coana CLI to v `14.12.107`.

## [1.1.38](https://github.com/SocketDev/socket-cli/releases/tag/v1.1.38) - 2025-11-26

### 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.38",
"version": "1.1.39",
"description": "CLI for Socket.dev",
"homepage": "https://github.com/SocketDev/socket-cli",
"license": "MIT AND OFL-1.1",
Expand Down Expand Up @@ -94,7 +94,7 @@
"@babel/preset-typescript": "7.27.1",
"@babel/runtime": "7.28.4",
"@biomejs/biome": "2.2.4",
"@coana-tech/cli": "14.12.101",
"@coana-tech/cli": "14.12.107",
"@cyclonedx/cdxgen": "11.11.0",
"@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.

25 changes: 22 additions & 3 deletions src/commands/scan/cmd-scan-reach.mts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ const generalFlags: MeowFlags = {
description:
'Force override the organization slug, overrides the default org from config',
},
output: {
type: 'string',
default: '',
description:
'Path to write the reachability report to (must end with .json). Defaults to .socket.facts.json in the current working directory.',
shortFlag: 'o',
},
}

export const cmdScanReach = {
Expand Down Expand Up @@ -84,7 +91,8 @@ async function run(
${getFlagListOutput(reachabilityFlags)}

Runs the Socket reachability analysis without creating a scan in Socket.
The output is written to .socket.facts.json in the current working directory.
The output is written to .socket.facts.json in the current working directory
unless the --output flag is specified.

Note: Manifest files are uploaded to Socket's backend services because the
reachability analysis requires creating a Software Bill of Materials (SBOM)
Expand All @@ -94,6 +102,8 @@ async function run(
$ ${command}
$ ${command} ./proj
$ ${command} ./proj --reach-ecosystems npm,pypi
$ ${command} --output custom-report.json
$ ${command} ./proj --output ./reports/analysis.json
`,
}

Expand All @@ -110,6 +120,7 @@ async function run(
json,
markdown,
org: orgFlag,
output: outputPath,
reachAnalysisMemoryLimit,
reachAnalysisTimeout,
reachConcurrency,
Expand All @@ -123,6 +134,7 @@ async function run(
json: boolean
markdown: boolean
org: string
output: string
reachAnalysisTimeout: number
reachAnalysisMemoryLimit: number
reachConcurrency: number
Expand Down Expand Up @@ -193,6 +205,12 @@ async function run(
message: 'The json and markdown flags cannot be both set, pick one',
fail: 'omit one',
},
{
nook: true,
test: !outputPath || outputPath.endsWith('.json'),
message: 'The --output path must end with .json',
fail: 'use a path ending with .json',
},
{
nook: true,
test: targetValidation.isValid,
Expand Down Expand Up @@ -229,10 +247,10 @@ async function run(

await handleScanReach({
cwd,
interactive,
orgSlug,
outputKind,
targets,
interactive,
outputPath: outputPath || '',
reachabilityOptions: {
reachAnalysisTimeout: Number(reachAnalysisTimeout),
reachAnalysisMemoryLimit: Number(reachAnalysisMemoryLimit),
Expand All @@ -244,5 +262,6 @@ async function run(
reachExcludePaths,
reachSkipCache: Boolean(reachSkipCache),
},
targets,
})
}
133 changes: 131 additions & 2 deletions src/commands/scan/cmd-scan-reach.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe('socket scan reach', async () => {
--json Output as JSON
--markdown Output as Markdown
--org Force override the organization slug, overrides the default org from config
--output Path to write the reachability report to (must end with .json). Defaults to .socket.facts.json in the current working directory.

Reachability Options
--reach-analysis-memory-limit The maximum memory in MB to use for the reachability analysis. The default is 8192MB.
Expand All @@ -47,7 +48,8 @@ describe('socket scan reach', async () => {
--reach-skip-cache Skip caching-based optimizations. By default, the reachability analysis will use cached configurations from previous runs to speed up the analysis.

Runs the Socket reachability analysis without creating a scan in Socket.
The output is written to .socket.facts.json in the current working directory.
The output is written to .socket.facts.json in the current working directory
unless the --output flag is specified.

Note: Manifest files are uploaded to Socket's backend services because the
reachability analysis requires creating a Software Bill of Materials (SBOM)
Expand All @@ -56,7 +58,9 @@ describe('socket scan reach', async () => {
Examples
$ socket scan reach
$ socket scan reach ./proj
$ socket scan reach ./proj --reach-ecosystems npm,pypi"
$ socket scan reach ./proj --reach-ecosystems npm,pypi
$ socket scan reach --output custom-report.json
$ socket scan reach ./proj --output ./reports/analysis.json"
`)
expect(`\n ${stderr}`).toMatchInlineSnapshot(`
"
Expand Down Expand Up @@ -763,6 +767,131 @@ describe('socket scan reach', async () => {
)
})

describe('output path tests', () => {
cmdit(
[
'scan',
'reach',
FLAG_DRY_RUN,
'--output',
'custom-report.json',
'--org',
'fakeOrg',
FLAG_CONFIG,
'{"apiToken":"fakeToken"}',
],
'should accept --output flag with .json extension',
async cmd => {
const { code, stdout } = await spawnSocketCli(binCliPath, cmd)
expect(stdout).toMatchInlineSnapshot(`"[DryRun]: Bailing now"`)
expect(code, 'should exit with code 0').toBe(0)
},
)

cmdit(
[
'scan',
'reach',
FLAG_DRY_RUN,
'-o',
'report.json',
'--org',
'fakeOrg',
FLAG_CONFIG,
'{"apiToken":"fakeToken"}',
],
'should accept -o short flag with .json extension',
async cmd => {
const { code, stdout } = await spawnSocketCli(binCliPath, cmd)
expect(stdout).toMatchInlineSnapshot(`"[DryRun]: Bailing now"`)
expect(code, 'should exit with code 0').toBe(0)
},
)

cmdit(
[
'scan',
'reach',
FLAG_DRY_RUN,
'--output',
'./reports/analysis.json',
'--org',
'fakeOrg',
FLAG_CONFIG,
'{"apiToken":"fakeToken"}',
],
'should accept --output flag with path',
async cmd => {
const { code, stdout } = await spawnSocketCli(binCliPath, cmd)
expect(stdout).toMatchInlineSnapshot(`"[DryRun]: Bailing now"`)
expect(code, 'should exit with code 0').toBe(0)
},
)

cmdit(
[
'scan',
'reach',
FLAG_DRY_RUN,
'--output',
'report.txt',
'--org',
'fakeOrg',
FLAG_CONFIG,
'{"apiToken":"fakeToken"}',
],
'should fail when --output does not end with .json',
async cmd => {
const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd)
const output = stdout + stderr
expect(output).toContain('The --output path must end with .json')
expect(code, 'should exit with non-zero code').not.toBe(0)
},
)

cmdit(
[
'scan',
'reach',
FLAG_DRY_RUN,
'--output',
'report',
'--org',
'fakeOrg',
FLAG_CONFIG,
'{"apiToken":"fakeToken"}',
],
'should fail when --output has no extension',
async cmd => {
const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd)
const output = stdout + stderr
expect(output).toContain('The --output path must end with .json')
expect(code, 'should exit with non-zero code').not.toBe(0)
},
)

cmdit(
[
'scan',
'reach',
FLAG_DRY_RUN,
'--output',
'report.JSON',
'--org',
'fakeOrg',
FLAG_CONFIG,
'{"apiToken":"fakeToken"}',
],
'should fail when --output ends with .JSON (uppercase)',
async cmd => {
const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd)
const output = stdout + stderr
expect(output).toContain('The --output path must end with .json')
expect(code, 'should exit with non-zero code').not.toBe(0)
},
)
})

describe('error handling and usability tests', () => {
cmdit(
[
Expand Down
10 changes: 8 additions & 2 deletions src/commands/scan/handle-scan-reach.mts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export type HandleScanReachConfig = {
interactive: boolean
orgSlug: string
outputKind: OutputKind
outputPath: string
reachabilityOptions: ReachabilityOptions
targets: string[]
}
Expand All @@ -25,6 +26,7 @@ export async function handleScanReach({
interactive: _interactive,
orgSlug,
outputKind,
outputPath,
reachabilityOptions,
targets,
}: HandleScanReachConfig) {
Expand All @@ -33,7 +35,10 @@ export async function handleScanReach({
// Get supported file names
const supportedFilesCResult = await fetchSupportedScanFileNames({ spinner })
if (!supportedFilesCResult.ok) {
await outputScanReach(supportedFilesCResult, { cwd, outputKind })
await outputScanReach(supportedFilesCResult, {
outputKind,
outputPath,
})
return
}

Expand Down Expand Up @@ -70,6 +75,7 @@ export async function handleScanReach({
const result = await performReachabilityAnalysis({
cwd,
orgSlug,
outputPath,
packagePaths,
reachabilityOptions,
spinner,
Expand All @@ -79,5 +85,5 @@ export async function handleScanReach({

spinner.stop()

await outputScanReach(result, { cwd, outputKind })
await outputScanReach(result, { outputKind, outputPath })
}
13 changes: 7 additions & 6 deletions src/commands/scan/output-scan-reach.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import path from 'node:path'

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

import constants from '../../constants.mts'
Expand All @@ -11,7 +9,10 @@ import type { CResult, OutputKind } from '../../types.mts'

export async function outputScanReach(
result: CResult<ReachabilityAnalysisResult>,
{ cwd, outputKind }: { cwd: string; outputKind: OutputKind },
{
outputKind,
outputPath,
}: { outputKind: OutputKind; outputPath: string },
): Promise<void> {
if (!result.ok) {
process.exitCode = result.code ?? 1
Expand All @@ -26,9 +27,9 @@ export async function outputScanReach(
return
}

const actualOutputPath = outputPath || constants.DOT_SOCKET_DOT_FACTS_JSON

logger.log('')
logger.success('Reachability analysis completed successfully!')
logger.info(
`Reachability report has been written to: ${path.join(cwd, constants.DOT_SOCKET_DOT_FACTS_JSON)}`,
)
logger.info(`Reachability report has been written to: ${actualOutputPath}`)
}
Loading