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
20 changes: 6 additions & 14 deletions src/commands/fix/coana-fix.mts
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,9 @@ import {
import { generateSocketFactsForFix } from './generated-socket-facts.mts'
import { getSocketFixBranchName, getSocketFixCommitMessage } from './git.mts'
import { getSocketFixPrs, openSocketFixPr } from './pull-request.mts'
import {
DOT_SOCKET_DOT_FACTS_JSON,
FLAG_DRY_RUN,
GQL_PR_STATE_OPEN,
} from '../../constants.mts'
import { FLAG_DRY_RUN, GQL_PR_STATE_OPEN } from '../../constants.mts'
import { handleApiCall } from '../../utils/api.mts'
import { isSocketFactsFile } from '../../utils/coana.mts'
import { findSocketYmlSync } from '../../utils/config.mts'
import { spawnCoanaDlx } from '../../utils/dlx.mts'
import { getErrorCause } from '../../utils/errors.mts'
Expand Down Expand Up @@ -194,10 +191,6 @@ async function discoverGhsaIds(
}
}

function isFactsFile(filepath: string): boolean {
return path.basename(filepath).toLowerCase() === DOT_SOCKET_DOT_FACTS_JSON
}

type GitWorkingTreeChanges = {
modified: string[]
untracked: string[]
Expand Down Expand Up @@ -379,16 +372,15 @@ async function coanaFixWithFacts(
cwd,
})
const scanFilepaths = await findScanFilepaths()
// Fail if any .socket.facts.json files are present in the scan folder.
// These are analysis artifacts and must be removed before re-running fix.
const factsFiles = scanFilepaths.filter(isFactsFile)
// Facts files are analysis artifacts and must be removed before re-running fix.
const factsFiles = scanFilepaths.filter(isSocketFactsFile)
if (factsFiles.length) {
if (!silence) {
spinner?.stop()
}
return {
ok: false,
message: `Found ${DOT_SOCKET_DOT_FACTS_JSON} in manifest files`,
message: 'Found Socket facts files in manifest files',
cause:
`Delete the following ${pluralize('file', factsFiles.length)} before running socket fix again:\n` +
factsFiles.map(p => ` - ${p}`).join('\n'),
Expand All @@ -409,7 +401,7 @@ async function coanaFixWithFacts(
})
} catch (e) {
// A failed build root aborts inference after others wrote their facts.
const partial = (await findScanFilepaths()).filter(isFactsFile)
const partial = (await findScanFilepaths()).filter(isSocketFactsFile)
await Promise.all(partial.map(p => fs.rm(p, { force: true })))
throw e
}
Expand Down
17 changes: 12 additions & 5 deletions src/commands/scan/cmd-scan-create.mts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { existsSync } from 'node:fs'
import { readdirSync } from 'node:fs'
import path from 'node:path'

import { logger } from '@socketsecurity/registry/lib/logger'
Expand Down Expand Up @@ -26,6 +26,7 @@ import constants, { REQUIREMENTS_TXT, SOCKET_JSON } from '../../constants.mts'
import { commonFlags, outputFlags } from '../../flags.mts'
import { checkCommandInput } from '../../utils/check-input.mts'
import { cmdFlagValueToArray } from '../../utils/cmd.mts'
import { isSocketFactsFile } from '../../utils/coana.mts'
import { determineOrgSlug } from '../../utils/determine-org-slug.mts'
import { parseReachEcosystems } from '../../utils/ecosystem.mts'
import { getOutputKind } from '../../utils/get-output-kind.mts'
Expand Down Expand Up @@ -184,6 +185,14 @@ const generalFlags: MeowFlags = {
},
}

function hasSocketFactsFileIn(dir: string): boolean {
try {
return readdirSync(dir).some(isSocketFactsFile)
} catch {
return false
}
}

export const cmdScanCreate = {
description,
hidden,
Expand Down Expand Up @@ -472,14 +481,12 @@ async function run(
}

const detected = await detectManifestActions(sockJson, cwd)
// Suppress the --auto-manifest suggestion when a `.socket.facts.json` is
// Suppress the --auto-manifest suggestion when a Socket facts file is
// already present at cwd. That file is the output of `socket manifest auto`
// (and `--facts` mode of the per-ecosystem manifest commands), so suggesting
// to regenerate it would be misleading; the manifest data is already there
// and will be picked up by the scan.
const hasFactsFile = existsSync(
path.join(cwd, constants.DOT_SOCKET_DOT_FACTS_JSON),
)
const hasFactsFile = hasSocketFactsFileIn(cwd)
if (
detected.count > 0 &&
!autoManifest &&
Expand Down
15 changes: 7 additions & 8 deletions src/commands/scan/handle-create-new-scan.mts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import constants from '../../constants.mts'
import { checkCommandInput } from '../../utils/check-input.mts'
import {
compressSocketFactsForUpload,
isSocketFactsFile,
snapshotSocketFacts,
} from '../../utils/coana.mts'
import { findSocketYmlSync } from '../../utils/config.mts'
Expand Down Expand Up @@ -197,7 +198,7 @@ async function createNewScan(

if (reach.dynamicSbomInference) {
// Already generated recursively above; resolving cwd's own build
// root a second time would race on the same .socket.facts.json.
// root a second time would race on the same facts file.
detected.gradle = false
detected.sbt = false
detected.maven = false
Expand Down Expand Up @@ -359,15 +360,13 @@ async function createNewScan(
reachabilityReport = reachResult.data?.reachabilityReport

// When using only pre-generated SBOMs, build the scan from those inputs —
// CycloneDX, SPDX, and Socket facts (`.socket.facts.json`) — matching
// Coana's `--use-only-pregenerated-sboms` selection. Otherwise drop any
// stray `.socket.facts.json`; coana's fresh reachability report (appended
// below) is the authoritative facts file for the scan.
// CycloneDX, SPDX, and Socket facts — matching Coana's
// `--use-only-pregenerated-sboms` selection. Otherwise drop every facts
// file; coana's fresh reachability report (appended below) is the
// authoritative facts file for the scan.
const pathsForScan = reach.reachUseOnlyPregeneratedSboms
? filterToPregeneratedSboms(packagePaths, supportedFiles)
: packagePaths.filter(
p => path.basename(p) !== constants.DOT_SOCKET_DOT_FACTS_JSON,
)
: packagePaths.filter(p => !isSocketFactsFile(p))

// Append coana's reachability report, but not twice: a pre-generated facts
// input can resolve to the same path coana wrote its report to.
Expand Down
28 changes: 28 additions & 0 deletions src/commands/scan/handle-create-new-scan.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,34 @@ describe('handleCreateNewScan excludePaths', () => {
expect(cleanup).toHaveBeenCalledOnce()
})

it('replaces every facts file input with the reachability report', async () => {
const cleanup = vi.fn()
const files = [
'/repo/pom.xml.socket.facts.json',
'/repo/gradle.socket.facts.json',
'/repo/service/.socket.facts.json',
'/repo/package-lock.json',
]
const config = createConfig({
generateScanFiles: async () => ({ cleanup, files }),
})
config.reach.runReachabilityAnalysis = true
mockPerformReachabilityAnalysis.mockResolvedValueOnce({
data: {
reachabilityReport: '.socket.facts.json',
tier1ReachabilityScanId: 'tier1-id',
},
ok: true,
})
await handleCreateNewScan(config)
expect(mockFetchCreateOrgFullScan).toHaveBeenCalledWith(
['/repo/package-lock.json', '.socket.facts.json'],
'fakeOrg',
expect.anything(),
expect.anything(),
)
})

it('includes generated auto-manifest files in SCA discovery targets', async () => {
mockGenerateAutoManifest.mockResolvedValueOnce({
generatedFiles: ['/repo/.socket-auto-manifest/maven_install.json'],
Expand Down
11 changes: 11 additions & 0 deletions src/utils/coana.mts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ export async function compressSocketFactsForUpload(
// remove a `.br` only to have it re-created after we returned.
const results = await Promise.allSettled(
scanPaths.map(async p => {
// depscan decodes only the bare `.socket.facts.json.br`; a named facts
// file is uploaded as plain JSON.
if (path.basename(p) !== DOT_SOCKET_DOT_FACTS_JSON) {
return p
}
Expand Down Expand Up @@ -119,6 +121,15 @@ export async function compressSocketFactsForUpload(
return { paths, cleanup }
}

// `.socket.facts.json` or a named `<entry>.socket.facts.json`, matching
// depscan's case-insensitive `*.socket.facts.json`.
export function isSocketFactsFile(filepath: string): boolean {
return path
.basename(filepath)
.toLowerCase()
.endsWith(DOT_SOCKET_DOT_FACTS_JSON)
}

export type ReachabilityError = {
componentName: string
componentVersion: string
Expand Down
35 changes: 35 additions & 0 deletions src/utils/coana.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
extractReachabilityErrors,
extractTier1ReachabilityScanId,
getFullWorkspacePath,
isSocketFactsFile,
snapshotSocketFacts,
} from './coana.mts'

Expand All @@ -53,6 +54,25 @@ describe('coana facts-file utils', () => {
return filePath
}

describe('isSocketFactsFile', () => {
it.each([
'.socket.facts.json',
'a/pom.xml.socket.facts.json',
'gradle.socket.facts.json',
'Foo.sln.SOCKET.FACTS.JSON',
])('matches %s', p => {
expect(isSocketFactsFile(p)).toBe(true)
})
it.each([
'socket.facts.json',
'.socket.facts.json.br',
'pom.xml',
'a/.socket.facts.json/pom.xml',
])('rejects %s', p => {
expect(isSocketFactsFile(p)).toBe(false)
})
})

describe('compressSocketFactsForUpload', () => {
it('writes brotli .br as a sibling of the source file', async () => {
const wrapDir = mkdtempSync(path.join(tmpdir(), 'socket-coana-wrap-'))
Expand Down Expand Up @@ -99,6 +119,21 @@ describe('coana facts-file utils', () => {
}
})

it('uploads a named facts file uncompressed', async () => {
const wrapDir = mkdtempSync(path.join(tmpdir(), 'socket-coana-wrap-'))
const facts = path.join(wrapDir, 'pom.xml.socket.facts.json')
writeFileSync(facts, '{}')

const result = await compressSocketFactsForUpload([facts])
try {
expect(result.paths).toEqual([facts])
expect(existsSync(`${facts}.br`)).toBe(false)
} finally {
await result.cleanup()
rmSync(wrapDir, { recursive: true, force: true })
}
})

it('leaves a missing .socket.facts.json path unchanged', async () => {
const wrapDir = mkdtempSync(path.join(tmpdir(), 'socket-coana-wrap-'))
const missingFacts = path.join(wrapDir, '.socket.facts.json')
Expand Down