Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
11 changes: 9 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,11 @@ export async function handleScanReach({
// Get supported file names
const supportedFilesCResult = await fetchSupportedScanFileNames({ spinner })
if (!supportedFilesCResult.ok) {
await outputScanReach(supportedFilesCResult, { cwd, outputKind })
await outputScanReach(supportedFilesCResult, {
cwd,
outputKind,
outputPath,
})
return
}

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

spinner.stop()

await outputScanReach(result, { cwd, outputKind })
await outputScanReach(result, { cwd, 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,
}: { cwd: string; outputKind: OutputKind; outputPath: string },
Comment thread
mtorp marked this conversation as resolved.
Outdated
): 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