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
28 changes: 28 additions & 0 deletions src/commands/scan/finalize-tier1-scan.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { sendApiRequest } from '../../utils/api.mts'

import type { CResult } from '../../types.mts'

export type FinalizeTier1ScanOptions = {
tier1_reachability_scan_id: string
report_run_id: string
}

/**
* Finalize a tier1 reachability scan.
* - Associates the tier1 reachability scan metadata with the full scan.
* - Sets the tier1 reachability scan to "finalized" state.
*/
export async function finalizeTier1Scan(
tier1_reachability_scan_id: string,
report_run_id: string,
): Promise<CResult<unknown>> {
// we do not use the SDK here because the tier1-reachability-scan/finalize is a hidden
// endpoint that is not part of the OpenAPI specification.
return await sendApiRequest('tier1-reachability-scan/finalize', {
method: 'POST',
body: {
tier1_reachability_scan_id,
report_run_id,
},
})
}
24 changes: 22 additions & 2 deletions src/commands/scan/handle-create-new-scan.mts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,16 @@ import { pluralize } from '@socketsecurity/registry/lib/words'

import { fetchCreateOrgFullScan } from './fetch-create-org-full-scan.mts'
import { fetchSupportedScanFileNames } from './fetch-supported-scan-file-names.mts'
import { finalizeTier1Scan } from './finalize-tier1-scan.mts'
import { handleScanReport } from './handle-scan-report.mts'
import { outputCreateNewScan } from './output-create-new-scan.mts'
import constants from '../../constants.mts'
import { handleApiCall } from '../../utils/api.mts'
import { checkCommandInput } from '../../utils/check-input.mts'
import { spawnCoana } from '../../utils/coana.mts'
import {
extractTier1ReachabilityScanId,
spawnCoana,
} from '../../utils/coana.mts'
import { getPackageFilesForScan } from '../../utils/path-resolve.mts'
import { setupSdk } from '../../utils/sdk.mts'
import { readOrDefaultSocketJson } from '../../utils/socketjson.mts'
Expand Down Expand Up @@ -112,6 +116,7 @@ export async function handleCreateNewScan({
}

let scanPaths: string[] = packagePaths
let tier1ReachabilityScanId: string | undefined

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

scanPaths = reachResult.data?.scanPaths || []
tier1ReachabilityScanId = reachResult.data?.tier1ReachabilityScanId
}

const fullScanCResult = await fetchCreateOrgFullScan(
Expand All @@ -152,6 +158,15 @@ export async function handleCreateNewScan({
},
)

if (
fullScanCResult.ok &&
reach &&
tier1ReachabilityScanId &&
fullScanCResult.data?.id
) {
await finalizeTier1Scan(tier1ReachabilityScanId, fullScanCResult.data?.id)
}

if (fullScanCResult.ok && report) {
if (fullScanCResult.data?.id) {
await handleScanReport({
Expand Down Expand Up @@ -195,7 +210,9 @@ async function performReachabilityAnalysis({
branchName: string
outputKind: OutputKind
interactive: boolean
}): Promise<CResult<{ scanPaths?: string[] }>> {
}): Promise<
CResult<{ scanPaths?: string[]; tier1ReachabilityScanId: string | undefined }>
> {
logger.info('Starting reachability analysis...')

packagePaths = packagePaths.filter(
Expand Down Expand Up @@ -275,6 +292,9 @@ async function performReachabilityAnalysis({
ok: true,
data: {
scanPaths: [constants.DOT_SOCKET_DOT_FACTS_JSON],
tier1ReachabilityScanId: extractTier1ReachabilityScanId(
constants.DOT_SOCKET_DOT_FACTS_JSON,
),
},
}
}
100 changes: 100 additions & 0 deletions src/utils/api.mts
Original file line number Diff line number Diff line change
Expand Up @@ -285,3 +285,103 @@ export async function queryApiSafeJson<T>(
}
}
}

export async function sendApiRequest<T>(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is all Claude.

We can probably make do with something simpler, but made it quite generic in case we need to use it from other places later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's cool. I may move it to the sdk lib just because it's a generic escape hatch for unpublished apis. Thank you @mtorp !

path: string,
options: {
method: 'POST' | 'PUT'
body?: unknown
fetchSpinnerDesc?: string
},
): Promise<CResult<T>> {
const apiToken = getDefaultToken()
if (!apiToken) {
return {
ok: false,
message: 'Authentication Error',
cause:
'User must be authenticated to run this command. To log in, run the command `socket login` and enter your Socket API token.',
}
}

const baseUrl = getDefaultApiBaseUrl() || ''
if (!baseUrl) {
logger.warn(
'API endpoint is not set and default was empty. Request is likely to fail.',
)
}

// Lazily access constants.spinner.
const { spinner } = constants

if (options.fetchSpinnerDesc) {
spinner.start(`Requesting ${options.fetchSpinnerDesc} from API...`)
}

let result
try {
const fetchOptions = {
method: options.method,
headers: {
Authorization: `Basic ${btoa(`${apiToken}:`)}`,
'Content-Type': 'application/json',
},
...(options.body ? { body: JSON.stringify(options.body) } : {}),
}

result = await fetch(
`${baseUrl}${baseUrl.endsWith('/') ? '' : '/'}${path}`,
fetchOptions,
)
if (options.fetchSpinnerDesc) {
spinner.successAndStop(
`Received Socket API response (after requesting ${options.fetchSpinnerDesc}).`,
)
}
} catch (e) {
if (options.fetchSpinnerDesc) {
spinner.failAndStop(
`An error was thrown while requesting ${options.fetchSpinnerDesc}.`,
)
}

const cause = (e as undefined | { message: string })?.message

debugFn('error', `caught: await fetch() ${options.method} error`)
debugDir('inspect', { error: e })

return {
ok: false,
message: 'API Request failed to complete',
...(cause ? { cause } : {}),
}
}

if (!result.ok) {
const cause = await getErrorMessageForHttpStatusCode(result.status)
return {
ok: false,
message: 'Socket API returned an error',
cause: `${result.statusText}${cause ? ` (cause: ${cause})` : ''}`,
data: {
code: result.status,
},
}
}

try {
const data = await result.json()
return {
ok: true,
data: data as T,
}
} catch (e) {
debugFn('error', 'caught: await result.json() error')
debugDir('inspect', { error: e })
return {
ok: false,
message: 'API Request failed to complete',
cause: 'There was an unexpected error trying to parse the response JSON',
}
}
}
16 changes: 15 additions & 1 deletion src/utils/coana.mts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { readFileSync } from 'node:fs'

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

import { getDefaultOrgSlug } from '../commands/ci/fetch-default-org-slug.mts'
Expand All @@ -14,7 +16,7 @@ export async function spawnCoana(
args: string[] | readonly string[],
options?: SpawnOptions | undefined,
extra?: SpawnExtra | undefined,
): Promise<CResult<unknown>> {
): Promise<CResult<string>> {
const { env: spawnEnv } = { __proto__: null, ...options } as SpawnOptions
const mixinsEnv: Record<string, string> = {
// Lazily access constants.ENV.INLINED_SOCKET_CLI_VERSION.
Expand Down Expand Up @@ -59,3 +61,15 @@ export async function spawnCoana(
return { ok: false, data: e, message }
}
}

export function extractTier1ReachabilityScanId(
socketFactsFile: string,
): string | undefined {
try {
const content = readFileSync(socketFactsFile, 'utf8')
const json = JSON.parse(content)
return json.tier1ReachabilityScanId
} catch {
return undefined
}
}
Loading