Skip to content

Commit 52ec667

Browse files
committed
call tier1-reachability-scan/finalize from the Socket CLI
1 parent 5095ed6 commit 52ec667

File tree

4 files changed

+169
-4
lines changed

4 files changed

+169
-4
lines changed
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { sendApiRequest } from '../../utils/api.mts'
2+
3+
import type { CResult } from '../../types.mts'
4+
5+
export type FinalizeTier1ScanOptions = {
6+
tier1_reachability_scan_id: string
7+
report_run_id: string
8+
}
9+
10+
/**
11+
* Finalize a tier1 reachability scan.
12+
* - Associates the tier1 reachability scan metadata with the full scan.
13+
* - Sets the tier1 reachability scan to "finalized" state.
14+
*/
15+
export async function finalizeTier1Scan(
16+
tier1_reachability_scan_id: string,
17+
report_run_id: string,
18+
): Promise<CResult<unknown>> {
19+
// we do not use the SDK here because the tier1-reachability-scan/finalize is a hidden
20+
// endpoint that is not part of the OpenAPI specification.
21+
return await sendApiRequest('tier1-reachability-scan/finalize', {
22+
method: 'POST',
23+
body: {
24+
tier1_reachability_scan_id,
25+
report_run_id,
26+
},
27+
})
28+
}

src/commands/scan/handle-create-new-scan.mts

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@ import { pluralize } from '@socketsecurity/registry/lib/words'
44

55
import { fetchCreateOrgFullScan } from './fetch-create-org-full-scan.mts'
66
import { fetchSupportedScanFileNames } from './fetch-supported-scan-file-names.mts'
7+
import { finalizeTier1Scan } from './finalize-tier1-scan.mts'
78
import { handleScanReport } from './handle-scan-report.mts'
89
import { outputCreateNewScan } from './output-create-new-scan.mts'
910
import constants from '../../constants.mts'
1011
import { handleApiCall } from '../../utils/api.mts'
1112
import { checkCommandInput } from '../../utils/check-input.mts'
12-
import { spawnCoana } from '../../utils/coana.mts'
13+
import {
14+
extractTier1ReachabilityScanId,
15+
spawnCoana,
16+
} from '../../utils/coana.mts'
1317
import { getPackageFilesForScan } from '../../utils/path-resolve.mts'
1418
import { setupSdk } from '../../utils/sdk.mts'
1519
import { readOrDefaultSocketJson } from '../../utils/socketjson.mts'
@@ -112,6 +116,7 @@ export async function handleCreateNewScan({
112116
}
113117

114118
let scanPaths: string[] = packagePaths
119+
let tier1ReachabilityScanId: string | undefined
115120

116121
// If reachability is enabled, perform reachability analysis
117122
if (reach) {
@@ -131,6 +136,7 @@ export async function handleCreateNewScan({
131136
}
132137

133138
scanPaths = reachResult.data?.scanPaths || []
139+
tier1ReachabilityScanId = reachResult.data?.tier1ReachabilityScanId
134140
}
135141

136142
const fullScanCResult = await fetchCreateOrgFullScan(
@@ -152,6 +158,15 @@ export async function handleCreateNewScan({
152158
},
153159
)
154160

161+
if (
162+
fullScanCResult.ok &&
163+
reach &&
164+
tier1ReachabilityScanId &&
165+
fullScanCResult.data?.id
166+
) {
167+
await finalizeTier1Scan(tier1ReachabilityScanId, fullScanCResult.data?.id)
168+
}
169+
155170
if (fullScanCResult.ok && report) {
156171
if (fullScanCResult.data?.id) {
157172
await handleScanReport({
@@ -195,7 +210,9 @@ async function performReachabilityAnalysis({
195210
branchName: string
196211
outputKind: OutputKind
197212
interactive: boolean
198-
}): Promise<CResult<{ scanPaths?: string[] }>> {
213+
}): Promise<
214+
CResult<{ scanPaths?: string[]; tier1ReachabilityScanId: string | undefined }>
215+
> {
199216
logger.info('Starting reachability analysis...')
200217

201218
packagePaths = packagePaths.filter(
@@ -275,6 +292,9 @@ async function performReachabilityAnalysis({
275292
ok: true,
276293
data: {
277294
scanPaths: [constants.DOT_SOCKET_DOT_FACTS_JSON],
295+
tier1ReachabilityScanId: extractTier1ReachabilityScanId(
296+
constants.DOT_SOCKET_DOT_FACTS_JSON,
297+
),
278298
},
279299
}
280300
}

src/utils/api.mts

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,3 +285,101 @@ export async function queryApiSafeJson<T>(
285285
}
286286
}
287287
}
288+
289+
export async function sendApiRequest<T>(
290+
path: string,
291+
options: {
292+
method: 'POST' | 'PUT'
293+
body?: unknown
294+
fetchSpinnerDesc?: string
295+
},
296+
): Promise<CResult<T>> {
297+
const apiToken = getDefaultToken()
298+
if (!apiToken) {
299+
return {
300+
ok: false,
301+
message: 'Authentication Error',
302+
cause:
303+
'User must be authenticated to run this command. To log in, run the command `socket login` and enter your Socket API token.',
304+
}
305+
}
306+
307+
const baseUrl = getDefaultApiBaseUrl() || ''
308+
if (!baseUrl) {
309+
logger.warn(
310+
'API endpoint is not set and default was empty. Request is likely to fail.',
311+
)
312+
}
313+
314+
// Lazily access constants.spinner.
315+
const { spinner } = constants
316+
317+
if (options.fetchSpinnerDesc) {
318+
spinner.start(`Requesting ${options.fetchSpinnerDesc} from API...`)
319+
}
320+
321+
let result
322+
try {
323+
result = await fetch(
324+
`${baseUrl}${baseUrl.endsWith('/') ? '' : '/'}${path}`,
325+
{
326+
method: options.method,
327+
headers: {
328+
Authorization: `Basic ${btoa(`${apiToken}:`)}`,
329+
'Content-Type': 'application/json',
330+
},
331+
body: options.body ? JSON.stringify(options.body) : undefined,
332+
},
333+
)
334+
if (options.fetchSpinnerDesc) {
335+
spinner.successAndStop(
336+
`Received Socket API response (after requesting ${options.fetchSpinnerDesc}).`,
337+
)
338+
}
339+
} catch (e) {
340+
if (options.fetchSpinnerDesc) {
341+
spinner.failAndStop(
342+
`An error was thrown while requesting ${options.fetchSpinnerDesc}.`,
343+
)
344+
}
345+
346+
const cause = (e as undefined | { message: string })?.message
347+
348+
debugFn('error', `caught: await fetch() ${options.method} error`)
349+
debugDir('inspect', { error: e })
350+
351+
return {
352+
ok: false,
353+
message: 'API Request failed to complete',
354+
...(cause ? { cause } : {}),
355+
}
356+
}
357+
358+
if (!result.ok) {
359+
const cause = await getErrorMessageForHttpStatusCode(result.status)
360+
return {
361+
ok: false,
362+
message: 'Socket API returned an error',
363+
cause: `${result.statusText}${cause ? ` (cause: ${cause})` : ''}`,
364+
data: {
365+
code: result.status,
366+
},
367+
}
368+
}
369+
370+
try {
371+
const data = await result.json()
372+
return {
373+
ok: true,
374+
data: data as T,
375+
}
376+
} catch (e) {
377+
debugFn('error', 'caught: await result.json() error')
378+
debugDir('inspect', { error: e })
379+
return {
380+
ok: false,
381+
message: 'API Request failed to complete',
382+
cause: 'There was an unexpected error trying to parse the response JSON',
383+
}
384+
}
385+
}

src/utils/coana.mts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { readFileSync } from 'node:fs'
2+
13
import { spawn } from '@socketsecurity/registry/lib/spawn'
24

35
import { getDefaultOrgSlug } from '../commands/ci/fetch-default-org-slug.mts'
@@ -14,7 +16,7 @@ export async function spawnCoana(
1416
args: string[] | readonly string[],
1517
options?: SpawnOptions | undefined,
1618
extra?: SpawnExtra | undefined,
17-
): Promise<CResult<unknown>> {
19+
): Promise<CResult<string>> {
1820
const { env: spawnEnv } = { __proto__: null, ...options } as SpawnOptions
1921
const mixinsEnv: Record<string, string> = {
2022
// Lazily access constants.ENV.INLINED_SOCKET_CLI_VERSION.
@@ -37,7 +39,8 @@ export async function spawnCoana(
3739
// Lazily access constants.nodeMemoryFlags.
3840
...constants.nodeMemoryFlags,
3941
// Lazily access constants.coanaBinPath.
40-
constants.coanaBinPath,
42+
// constants.coanaBinPath,
43+
'/Users/martintorp/coana/coana-package-manager/packages/cli/dist/index.js',
4144
...args,
4245
],
4346
{
@@ -52,10 +55,26 @@ export async function spawnCoana(
5255
},
5356
extra,
5457
)
58+
console.log('SPAWNED COANA', output.stdout)
5559
return { ok: true, data: output.stdout }
5660
} catch (e) {
5761
const stderr = (e as any)?.stderr
5862
const message = stderr ? stderr : (e as Error)?.message
5963
return { ok: false, data: e, message }
6064
}
6165
}
66+
67+
export function extractTier1ReachabilityScanId(
68+
socketFactsFile: string,
69+
): string | undefined {
70+
try {
71+
console.log('socketFactsFile', socketFactsFile)
72+
const content = readFileSync(socketFactsFile, 'utf8')
73+
console.log('content', content)
74+
const json = JSON.parse(content)
75+
console.log('json', json)
76+
return json.tier1ReachabilityScanId
77+
} catch {
78+
return undefined
79+
}
80+
}

0 commit comments

Comments
 (0)