Skip to content
Closed
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
19 changes: 9 additions & 10 deletions src/commands/fix/coana-fix.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,9 @@ import { joinAnd } from '@socketsecurity/lib/arrays'
import { debug, debugDir } from '@socketsecurity/lib/debug'
import { logger } from '@socketsecurity/lib/logger'
import { pluralize } from '@socketsecurity/lib/words'

import {
checkCiEnvVars,
getCiEnvInstructions,
getFixEnv,
} from './env-helpers.mts'
import { getSocketFixBranchName, getSocketFixCommitMessage } from './git.mts'
import { getSocketFixPrs, openSocketFixPr } from './pull-request.mts'
import { FLAG_DRY_RUN } from '../../constants/cli.mts'
import { GQL_PR_STATE_OPEN } from '../../constants/github.mts'
import type { CResult } from '../../types.mts'
import { spawnCoanaDlx } from '../../utils/dlx/spawn.mjs'
import { getErrorCause } from '../../utils/error/errors.mjs'
import { getPackageFilesForScan } from '../../utils/fs/path-resolve.mjs'
Expand All @@ -36,9 +29,14 @@ import { cmdFlagValueToArray } from '../../utils/process/cmd.mts'
import { handleApiCall } from '../../utils/socket/api.mjs'
import { setupSdk } from '../../utils/socket/sdk.mjs'
import { fetchSupportedScanFileNames } from '../scan/fetch-supported-scan-file-names.mts'

import {
checkCiEnvVars,
getCiEnvInstructions,
getFixEnv,
} from './env-helpers.mts'
import { getSocketFixBranchName, getSocketFixCommitMessage } from './git.mts'
import { getSocketFixPrs, openSocketFixPr } from './pull-request.mts'
import type { FixConfig } from './types.mts'
import type { CResult } from '../../types.mts'

export async function coanaFix(
fixConfig: FixConfig,
Expand Down Expand Up @@ -392,6 +390,7 @@ export async function coanaFix(
const prRef = `PR #${data.number}`

logger.success(`Opened ${prRef} for ${ghsaId}.`)
logger.info(`PR URL: ${data.html_url}`)

if (autopilot) {
logger.indent()
Expand Down
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 @@ -47,6 +47,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 @@ -83,7 +90,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 @@ -93,6 +101,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 @@ -109,6 +119,7 @@ async function run(
json,
markdown,
org: orgFlag,
output: outputPath,
reachAnalysisMemoryLimit,
reachAnalysisTimeout,
reachDisableAnalytics,
Expand All @@ -119,6 +130,7 @@ async function run(
json: boolean
markdown: boolean
org: string
output: string
reachAnalysisTimeout: number
reachAnalysisMemoryLimit: number
reachDisableAnalytics: boolean
Expand Down Expand Up @@ -183,6 +195,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',
},
)
if (!wasValidInput) {
return
Expand All @@ -195,10 +213,10 @@ async function run(

await handleScanReach({
cwd,
interactive,
orgSlug,
outputKind,
targets,
interactive,
outputPath: outputPath || '',
reachabilityOptions: {
reachAnalysisTimeout: Number(reachAnalysisTimeout),
reachAnalysisMemoryLimit: Number(reachAnalysisMemoryLimit),
Expand All @@ -207,5 +225,6 @@ async function run(
reachExcludePaths,
reachSkipCache: Boolean(reachSkipCache),
},
targets,
})
}
129 changes: 127 additions & 2 deletions src/commands/scan/cmd-scan-reach.test.mts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import path from 'node:path'

import { describe, expect, it } from 'vitest'

import { cmdit, spawnSocketCli, testPath } from '../../../test/utils.mts'
Expand All @@ -10,7 +9,8 @@ const binCliPath = getBinCliPath()

const _fixtureBaseDir = path.join(testPath, 'fixtures/commands/scan/reach')

describe('socket scan reach', async () => {cmdit(
describe('socket scan reach', async () => {
cmdit(
['scan', 'reach', FLAG_HELP, FLAG_CONFIG, '{}'],
`should support ${FLAG_HELP}`,
async cmd => {
Expand Down Expand Up @@ -673,6 +673,131 @@ describe('socket scan reach', async () => {cmdit(
)
})

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 @@ -78,5 +85,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/lib/logger'

import { DOT_SOCKET_DOT_FACTS_JSON } from '../../constants/paths.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 },
): Promise<void> {
if (!result.ok) {
process.exitCode = result.code ?? 1
Expand All @@ -26,9 +27,9 @@ export async function outputScanReach(
return
}

const actualOutputPath = outputPath ?? DOT_SOCKET_DOT_FACTS_JSON

logger.log('')
logger.success('Reachability analysis completed successfully!')
logger.info(
`Reachability report has been written to: ${path.join(cwd, DOT_SOCKET_DOT_FACTS_JSON)}`,
)
logger.info(`Reachability report has been written to: ${actualOutputPath}`)
}
16 changes: 9 additions & 7 deletions src/commands/scan/perform-reachability-analysis.mts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export type ReachabilityAnalysisOptions = {
branchName?: string | undefined
cwd?: string | undefined
orgSlug?: string | undefined
outputPath?: string | undefined
packagePaths?: string[] | undefined
reachabilityOptions: ReachabilityOptions
repoName?: string | undefined
Expand All @@ -49,6 +50,7 @@ export async function performReachabilityAnalysis(
branchName,
cwd = process.cwd(),
orgSlug,
outputPath,
packagePaths,
reachabilityOptions,
repoName,
Expand Down Expand Up @@ -134,14 +136,15 @@ export async function performReachabilityAnalysis(
spinner?.start()
spinner?.infoAndStop('Running reachability analysis with Coana...')

const outputFilePath = outputPath ?? DOT_SOCKET_DOT_FACTS_JSON
// Build Coana arguments.
const coanaArgs = [
'run',
cwd,
'--output-dir',
cwd,
path.dirname(outputFilePath),
'--socket-mode',
DOT_SOCKET_DOT_FACTS_JSON,
outputFilePath,
'--disable-report-submission',
...(reachabilityOptions.reachAnalysisTimeout
? ['--analysis-timeout', `${reachabilityOptions.reachAnalysisTimeout}`]
Expand Down Expand Up @@ -192,11 +195,10 @@ export async function performReachabilityAnalysis(
? {
ok: true,
data: {
// Use the DOT_SOCKET_DOT_FACTS_JSON file for the scan.
reachabilityReport: DOT_SOCKET_DOT_FACTS_JSON,
tier1ReachabilityScanId: extractTier1ReachabilityScanId(
DOT_SOCKET_DOT_FACTS_JSON,
),
// Use the actual output filename for the scan.
reachabilityReport: outputFilePath,
tier1ReachabilityScanId:
extractTier1ReachabilityScanId(outputFilePath),
},
}
: coanaResult
Expand Down
Loading