Skip to content
Draft
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
57 changes: 35 additions & 22 deletions src/commands/scan/handle-create-new-scan.mts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { outputCreateNewScan } from './output-create-new-scan.mts'
import { performReachabilityAnalysis } from './perform-reachability-analysis.mts'
import constants from '../../constants.mts'
import { checkCommandInput } from '../../utils/check-input.mts'
import { compressSocketFactsForUpload } from '../../utils/coana.mts'
import { findSocketYmlSync } from '../../utils/config.mts'
import { getPackageFilesForScan } from '../../utils/path-resolve.mts'
import { readOrDefaultSocketJson } from '../../utils/socket-json.mts'
Expand Down Expand Up @@ -259,28 +260,40 @@ export async function handleCreateNewScan({
tier1ReachabilityScanId = reachResult.data?.tier1ReachabilityScanId
}

const fullScanCResult = await fetchCreateOrgFullScan(
scanPaths,
orgSlug,
{
commitHash,
commitMessage,
committers,
pullRequest,
repoName,
branchName,
scanType: reach.runReachabilityAnalysis
? constants.SCAN_TYPE_SOCKET_TIER1
: constants.SCAN_TYPE_SOCKET,
workspace,
},
{
cwd,
defaultBranch,
pendingHead,
tmp,
},
)
// Brotli-compress any .socket.facts.json paths in scanPaths just before
// upload. depscan's api-v0 multipart boundary streams brotli decode based
// on the .br filename suffix. Coana keeps writing plain .socket.facts.json
// on disk, so the local read paths (extractTier1ReachabilityScanId,
// extractReachabilityErrors) stay correct. The cleanup() in the finally
// block removes the temp dirs whether the upload succeeded or threw.
const compressed = await compressSocketFactsForUpload(scanPaths)
let fullScanCResult: Awaited<ReturnType<typeof fetchCreateOrgFullScan>>
try {
fullScanCResult = await fetchCreateOrgFullScan(
compressed.paths,
orgSlug,
{
commitHash,
commitMessage,
committers,
pullRequest,
repoName,
branchName,
scanType: reach.runReachabilityAnalysis
? constants.SCAN_TYPE_SOCKET_TIER1
: constants.SCAN_TYPE_SOCKET,
workspace,
},
{
cwd,
defaultBranch,
pendingHead,
tmp,
},
)
} finally {
await compressed.cleanup()
}

const scanId = fullScanCResult.ok ? fullScanCResult.data?.id : undefined

Expand Down
81 changes: 81 additions & 0 deletions src/utils/coana.mts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@
* Manages reachability analysis via Coana tech CLI.
*
* Key Functions:
* - compressSocketFactsForUpload: Brotli-compress any .socket.facts.json
* entries in scanPaths just before upload, returning swapped paths plus a
* cleanup callback. Coana keeps writing plain JSON; the on-the-wire form
* to depscan is brotli (api-v0 decodes at the multipart boundary).
* - extractReachabilityErrors: Extract per-component reachability errors
* - extractTier1ReachabilityScanId: Extract scan ID from socket facts file
*
* Integration:
Expand All @@ -11,8 +16,84 @@
* - Extracts tier 1 reachability scan identifiers
*/

import { createReadStream, createWriteStream, existsSync } from 'node:fs'
import { rm } from 'node:fs/promises'
import path from 'node:path'
import { pipeline } from 'node:stream/promises'
import { createBrotliCompress } from 'node:zlib'

import { readJsonSync } from '@socketsecurity/registry/lib/fs'

import constants from '../constants.mts'

const { DOT_SOCKET_DOT_FACTS_JSON } = constants

export type CompressedScanPaths = {
paths: string[]
cleanup: () => Promise<void>
}

/**
* For each `.socket.facts.json` in `scanPaths`, stream-brotli-compress a
* sibling `.socket.facts.json.br` next to the original file and swap its
* path in. Other paths pass through unchanged. Missing files also pass
* through unchanged (the upload will fail downstream with the same error
* it would have).
*
* Streaming + worker-thread compression keeps the event loop responsive:
* default brotli quality (11) on a 60+MB facts file takes multiple seconds
* of CPU, which would otherwise freeze the spinner / signal handlers /
* any concurrent work.
*
* The `.br` lives next to the source rather than under the OS temp dir
* because depscan's multipart ingest (`addStreamEntry`) rejects entries
* whose names contain `..` traversal segments. The SDK computes the
* multipart entry name via `path.relative(cwd, brPath)`, so an OS-tmpdir
* temp path turns into `../../../var/folders/...` and gets dropped as
* `unmatchedFiles`. Sibling-write keeps the relative path inside cwd, and
* keeps the directory shape symmetric with the plain `.socket.facts.json`
* upload (depscan strips only the `.br` suffix at ingest, so
* `<dir>/.socket.facts.json.br` and `<dir>/.socket.facts.json` resolve to
* the same storage path).
*
* Concurrent scans against the same source directory are already racy on
* `.socket.facts.json` itself (coana writes to a single path), so the
* sibling `.br` doesn't introduce a new race.
*
* Caller MUST `await cleanup()` (typically in a `finally` block) once the
* upload completes — successful or not — to remove the sibling files.
*/
export async function compressSocketFactsForUpload(
scanPaths: string[],
): Promise<CompressedScanPaths> {
const brPaths: string[] = []
const paths = await Promise.all(
scanPaths.map(async p => {
if (path.basename(p) !== DOT_SOCKET_DOT_FACTS_JSON) {
return p
}
if (!existsSync(p)) {
return p
}
const brPath = `${p}.br`
await pipeline(
createReadStream(p),
createBrotliCompress(),
createWriteStream(brPath),
)
brPaths.push(brPath)
return brPath
}),
)
const cleanup = async () => {
const targets = brPaths.splice(0)
await Promise.all(
targets.map(t => rm(t, { force: true })),
)
}
return { paths, cleanup }
}

export type ReachabilityError = {
componentName: string
componentVersion: string
Expand Down
Loading
Loading