Skip to content

Commit ffca4be

Browse files
committed
add --output / -o option to override the default .socket.facts.json output file for socket scan reach
1 parent 6380947 commit ffca4be

File tree

5 files changed

+180
-22
lines changed

5 files changed

+180
-22
lines changed

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

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ const generalFlags: MeowFlags = {
4747
description:
4848
'Force override the organization slug, overrides the default org from config',
4949
},
50+
output: {
51+
type: 'string',
52+
default: '',
53+
description:
54+
'Path to write the reachability report to (must end with .json). Defaults to .socket.facts.json in the current working directory.',
55+
shortFlag: 'o',
56+
},
5057
}
5158

5259
export const cmdScanReach = {
@@ -83,7 +90,8 @@ async function run(
8390
${getFlagListOutput(reachabilityFlags)}
8491
8592
Runs the Socket reachability analysis without creating a scan in Socket.
86-
The output is written to .socket.facts.json in the current working directory.
93+
The output is written to .socket.facts.json in the current working directory
94+
unless the --output flag is specified.
8795
8896
Note: Manifest files are uploaded to Socket's backend services because the
8997
reachability analysis requires creating a Software Bill of Materials (SBOM)
@@ -93,6 +101,8 @@ async function run(
93101
$ ${command}
94102
$ ${command} ./proj
95103
$ ${command} ./proj --reach-ecosystems npm,pypi
104+
$ ${command} --output custom-report.json
105+
$ ${command} ./proj --output ./reports/analysis.json
96106
`,
97107
}
98108

@@ -109,6 +119,7 @@ async function run(
109119
json,
110120
markdown,
111121
org: orgFlag,
122+
output: outputPath,
112123
reachAnalysisMemoryLimit,
113124
reachAnalysisTimeout,
114125
reachDisableAnalytics,
@@ -119,6 +130,7 @@ async function run(
119130
json: boolean
120131
markdown: boolean
121132
org: string
133+
output: string
122134
reachAnalysisTimeout: number
123135
reachAnalysisMemoryLimit: number
124136
reachDisableAnalytics: boolean
@@ -183,6 +195,12 @@ async function run(
183195
message: 'The json and markdown flags cannot be both set, pick one',
184196
fail: 'omit one',
185197
},
198+
{
199+
nook: true,
200+
test: !outputPath || outputPath.endsWith('.json'),
201+
message: 'The --output path must end with .json',
202+
fail: 'use a path ending with .json',
203+
},
186204
)
187205
if (!wasValidInput) {
188206
return
@@ -195,10 +213,10 @@ async function run(
195213

196214
await handleScanReach({
197215
cwd,
216+
interactive,
198217
orgSlug,
199218
outputKind,
200-
targets,
201-
interactive,
219+
outputPath: outputPath || '',
202220
reachabilityOptions: {
203221
reachAnalysisTimeout: Number(reachAnalysisTimeout),
204222
reachAnalysisMemoryLimit: Number(reachAnalysisMemoryLimit),
@@ -207,5 +225,6 @@ async function run(
207225
reachExcludePaths,
208226
reachSkipCache: Boolean(reachSkipCache),
209227
},
228+
targets,
210229
})
211230
}

src/commands/scan/cmd-scan-reach.test.mts

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ describe('socket scan reach', async () => {
3434
--json Output as JSON
3535
--markdown Output as Markdown
3636
--org Force override the organization slug, overrides the default org from config
37+
--output Path to write the reachability report to (must end with .json). Defaults to .socket.facts.json in the current working directory.
3738
3839
Reachability Options
3940
--reach-analysis-memory-limit The maximum memory in MB to use for the reachability analysis. The default is 8192MB.
@@ -44,7 +45,8 @@ describe('socket scan reach', async () => {
4445
--reach-skip-cache Skip caching-based optimizations. By default, the reachability analysis will use cached configurations from previous runs to speed up the analysis.
4546
4647
Runs the Socket reachability analysis without creating a scan in Socket.
47-
The output is written to .socket.facts.json in the current working directory.
48+
The output is written to .socket.facts.json in the current working directory
49+
unless the --output flag is specified.
4850
4951
Note: Manifest files are uploaded to Socket's backend services because the
5052
reachability analysis requires creating a Software Bill of Materials (SBOM)
@@ -53,7 +55,9 @@ describe('socket scan reach', async () => {
5355
Examples
5456
$ socket scan reach
5557
$ socket scan reach ./proj
56-
$ socket scan reach ./proj --reach-ecosystems npm,pypi"
58+
$ socket scan reach ./proj --reach-ecosystems npm,pypi
59+
$ socket scan reach --output custom-report.json
60+
$ socket scan reach ./proj --output ./reports/analysis.json"
5761
`)
5862
expect(`\n ${stderr}`).toMatchInlineSnapshot(`
5963
"
@@ -718,6 +722,131 @@ describe('socket scan reach', async () => {
718722
)
719723
})
720724

725+
describe('output path tests', () => {
726+
cmdit(
727+
[
728+
'scan',
729+
'reach',
730+
FLAG_DRY_RUN,
731+
'--output',
732+
'custom-report.json',
733+
'--org',
734+
'fakeOrg',
735+
FLAG_CONFIG,
736+
'{"apiToken":"fakeToken"}',
737+
],
738+
'should accept --output flag with .json extension',
739+
async cmd => {
740+
const { code, stdout } = await spawnSocketCli(binCliPath, cmd)
741+
expect(stdout).toMatchInlineSnapshot(`"[DryRun]: Bailing now"`)
742+
expect(code, 'should exit with code 0').toBe(0)
743+
},
744+
)
745+
746+
cmdit(
747+
[
748+
'scan',
749+
'reach',
750+
FLAG_DRY_RUN,
751+
'-o',
752+
'report.json',
753+
'--org',
754+
'fakeOrg',
755+
FLAG_CONFIG,
756+
'{"apiToken":"fakeToken"}',
757+
],
758+
'should accept -o short flag with .json extension',
759+
async cmd => {
760+
const { code, stdout } = await spawnSocketCli(binCliPath, cmd)
761+
expect(stdout).toMatchInlineSnapshot(`"[DryRun]: Bailing now"`)
762+
expect(code, 'should exit with code 0').toBe(0)
763+
},
764+
)
765+
766+
cmdit(
767+
[
768+
'scan',
769+
'reach',
770+
FLAG_DRY_RUN,
771+
'--output',
772+
'./reports/analysis.json',
773+
'--org',
774+
'fakeOrg',
775+
FLAG_CONFIG,
776+
'{"apiToken":"fakeToken"}',
777+
],
778+
'should accept --output flag with path',
779+
async cmd => {
780+
const { code, stdout } = await spawnSocketCli(binCliPath, cmd)
781+
expect(stdout).toMatchInlineSnapshot(`"[DryRun]: Bailing now"`)
782+
expect(code, 'should exit with code 0').toBe(0)
783+
},
784+
)
785+
786+
cmdit(
787+
[
788+
'scan',
789+
'reach',
790+
FLAG_DRY_RUN,
791+
'--output',
792+
'report.txt',
793+
'--org',
794+
'fakeOrg',
795+
FLAG_CONFIG,
796+
'{"apiToken":"fakeToken"}',
797+
],
798+
'should fail when --output does not end with .json',
799+
async cmd => {
800+
const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd)
801+
const output = stdout + stderr
802+
expect(output).toContain('The --output path must end with .json')
803+
expect(code, 'should exit with non-zero code').not.toBe(0)
804+
},
805+
)
806+
807+
cmdit(
808+
[
809+
'scan',
810+
'reach',
811+
FLAG_DRY_RUN,
812+
'--output',
813+
'report',
814+
'--org',
815+
'fakeOrg',
816+
FLAG_CONFIG,
817+
'{"apiToken":"fakeToken"}',
818+
],
819+
'should fail when --output has no extension',
820+
async cmd => {
821+
const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd)
822+
const output = stdout + stderr
823+
expect(output).toContain('The --output path must end with .json')
824+
expect(code, 'should exit with non-zero code').not.toBe(0)
825+
},
826+
)
827+
828+
cmdit(
829+
[
830+
'scan',
831+
'reach',
832+
FLAG_DRY_RUN,
833+
'--output',
834+
'report.JSON',
835+
'--org',
836+
'fakeOrg',
837+
FLAG_CONFIG,
838+
'{"apiToken":"fakeToken"}',
839+
],
840+
'should fail when --output ends with .JSON (uppercase)',
841+
async cmd => {
842+
const { code, stderr, stdout } = await spawnSocketCli(binCliPath, cmd)
843+
const output = stdout + stderr
844+
expect(output).toContain('The --output path must end with .json')
845+
expect(code, 'should exit with non-zero code').not.toBe(0)
846+
},
847+
)
848+
})
849+
721850
describe('error handling and usability tests', () => {
722851
cmdit(
723852
[

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

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type HandleScanReachConfig = {
1616
interactive: boolean
1717
orgSlug: string
1818
outputKind: OutputKind
19+
outputPath: string
1920
reachabilityOptions: ReachabilityOptions
2021
targets: string[]
2122
}
@@ -25,6 +26,7 @@ export async function handleScanReach({
2526
interactive: _interactive,
2627
orgSlug,
2728
outputKind,
29+
outputPath,
2830
reachabilityOptions,
2931
targets,
3032
}: HandleScanReachConfig) {
@@ -33,7 +35,11 @@ export async function handleScanReach({
3335
// Get supported file names
3436
const supportedFilesCResult = await fetchSupportedScanFileNames({ spinner })
3537
if (!supportedFilesCResult.ok) {
36-
await outputScanReach(supportedFilesCResult, { cwd, outputKind })
38+
await outputScanReach(supportedFilesCResult, {
39+
cwd,
40+
outputKind,
41+
outputPath,
42+
})
3743
return
3844
}
3945

@@ -70,6 +76,7 @@ export async function handleScanReach({
7076
const result = await performReachabilityAnalysis({
7177
cwd,
7278
orgSlug,
79+
outputPath,
7380
packagePaths,
7481
reachabilityOptions,
7582
spinner,
@@ -78,5 +85,5 @@ export async function handleScanReach({
7885

7986
spinner.stop()
8087

81-
await outputScanReach(result, { cwd, outputKind })
88+
await outputScanReach(result, { cwd, outputKind, outputPath })
8289
}

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

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
1-
import path from 'node:path'
2-
31
import { logger } from '@socketsecurity/registry/lib/logger'
42

5-
import constants from '../../constants.mts'
3+
import { DOT_SOCKET_DOT_FACTS_JSON } from '../../constants.mts'
64
import { failMsgWithBadge } from '../../utils/fail-msg-with-badge.mts'
75
import { serializeResultJson } from '../../utils/serialize-result-json.mts'
86

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

1210
export async function outputScanReach(
1311
result: CResult<ReachabilityAnalysisResult>,
14-
{ cwd, outputKind }: { cwd: string; outputKind: OutputKind },
12+
{
13+
outputKind,
14+
outputPath,
15+
}: { cwd: string; outputKind: OutputKind; outputPath: string },
1516
): Promise<void> {
1617
if (!result.ok) {
1718
process.exitCode = result.code ?? 1
@@ -26,9 +27,9 @@ export async function outputScanReach(
2627
return
2728
}
2829

30+
const actualOutputPath = outputPath ?? DOT_SOCKET_DOT_FACTS_JSON
31+
2932
logger.log('')
3033
logger.success('Reachability analysis completed successfully!')
31-
logger.info(
32-
`Reachability report has been written to: ${path.join(cwd, constants.DOT_SOCKET_DOT_FACTS_JSON)}`,
33-
)
34+
logger.info(`Reachability report has been written to: ${actualOutputPath}`)
3435
}

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

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import path from 'node:path'
22

3-
import constants from '../../constants.mts'
3+
import constants, { DOT_SOCKET_DOT_FACTS_JSON } from '../../constants.mts'
44
import { handleApiCall } from '../../utils/api.mts'
55
import { extractTier1ReachabilityScanId } from '../../utils/coana.mts'
66
import { spawnCoanaDlx } from '../../utils/dlx.mts'
@@ -26,6 +26,7 @@ export type ReachabilityAnalysisOptions = {
2626
branchName?: string | undefined
2727
cwd?: string | undefined
2828
orgSlug?: string | undefined
29+
outputPath?: string | undefined
2930
packagePaths?: string[] | undefined
3031
reachabilityOptions: ReachabilityOptions
3132
repoName?: string | undefined
@@ -45,6 +46,7 @@ export async function performReachabilityAnalysis(
4546
branchName,
4647
cwd = process.cwd(),
4748
orgSlug,
49+
outputPath,
4850
packagePaths,
4951
reachabilityOptions,
5052
repoName,
@@ -131,14 +133,15 @@ export async function performReachabilityAnalysis(
131133
spinner?.start()
132134
spinner?.infoAndStop('Running reachability analysis with Coana...')
133135

136+
const outputFilePath = outputPath ?? DOT_SOCKET_DOT_FACTS_JSON
134137
// Build Coana arguments.
135138
const coanaArgs = [
136139
'run',
137140
cwd,
138141
'--output-dir',
139-
cwd,
142+
path.dirname(outputFilePath),
140143
'--socket-mode',
141-
constants.DOT_SOCKET_DOT_FACTS_JSON,
144+
outputFilePath,
142145
'--disable-report-submission',
143146
...(reachabilityOptions.reachAnalysisTimeout
144147
? ['--analysis-timeout', `${reachabilityOptions.reachAnalysisTimeout}`]
@@ -189,11 +192,10 @@ export async function performReachabilityAnalysis(
189192
? {
190193
ok: true,
191194
data: {
192-
// Use the DOT_SOCKET_DOT_FACTS_JSON file for the scan.
193-
reachabilityReport: constants.DOT_SOCKET_DOT_FACTS_JSON,
194-
tier1ReachabilityScanId: extractTier1ReachabilityScanId(
195-
constants.DOT_SOCKET_DOT_FACTS_JSON,
196-
),
195+
// Use the actual output filename for the scan.
196+
reachabilityReport: outputFilePath,
197+
tier1ReachabilityScanId:
198+
extractTier1ReachabilityScanId(outputFilePath),
197199
},
198200
}
199201
: coanaResult

0 commit comments

Comments
 (0)