1- import { debugDir } from '@socketsecurity/registry/lib/debug '
1+ import path from 'node:path '
22
3+ import { debugDir , debugFn } from '@socketsecurity/registry/lib/debug'
4+ import { logger } from '@socketsecurity/registry/lib/logger'
5+
6+ import { getFixEnv } from './fix-env-helpers.mts'
7+ import {
8+ enablePrAutoMerge ,
9+ openCoanaPr ,
10+ setGitRemoteGithubRepoUrl ,
11+ } from './pull-request.mts'
312import { handleApiCall } from '../../utils/api.mts'
13+ import { cmdFlagValueToArray } from '../../utils/cmd.mts'
414import { spawnCoana } from '../../utils/coana.mts'
15+ import {
16+ gitCheckoutBranch ,
17+ gitCommit ,
18+ gitCreateBranch ,
19+ gitDeleteBranch ,
20+ gitPushBranch ,
21+ gitRemoteBranchExists ,
22+ gitResetAndClean ,
23+ gitUnstagedModifiedFiles ,
24+ } from '../../utils/git.mts'
525import { getPackageFilesForScan } from '../../utils/path-resolve.mts'
626import { setupSdk } from '../../utils/sdk.mts'
727import { fetchSupportedScanFileNames } from '../scan/fetch-supported-scan-file-names.mts'
@@ -12,90 +32,278 @@ import type { CResult } from '../../types.mts'
1232export async function coanaFix (
1333 fixConfig : FixConfig ,
1434) : Promise < CResult < { fixed : boolean } > > {
15- const { ghsas } = fixConfig
16-
17- if ( ! ghsas . length ) {
18- return { ok : true , data : { fixed : false } }
19- }
35+ const { autoMerge, cwd, ghsas, limit, orgSlug, spinner } = fixConfig
2036
21- const { cwd, orgSlug, spinner } = fixConfig
37+ const fixEnv = await getFixEnv ( )
38+ debugDir ( 'inspect' , { fixEnv } )
2239
2340 spinner ?. start ( )
2441
2542 const sockSdkCResult = await setupSdk ( )
43+ if ( ! sockSdkCResult . ok ) {
44+ return sockSdkCResult
45+ }
46+
47+ const sockSdk = sockSdkCResult . data
2648
27- let lastCResult : CResult < any > = sockSdkCResult
49+ const supportedFilesCResult = await fetchSupportedScanFileNames ( )
50+ if ( ! supportedFilesCResult . ok ) {
51+ return supportedFilesCResult
52+ }
2853
29- const sockSdk = sockSdkCResult . ok ? sockSdkCResult . data : undefined
54+ const supportedFiles = supportedFilesCResult . data
55+ const scanFilepaths = await getPackageFilesForScan ( [ '.' ] , supportedFiles , {
56+ cwd,
57+ } )
58+ const uploadCResult = await handleApiCall (
59+ sockSdk . uploadManifestFiles ( orgSlug , scanFilepaths ) ,
60+ {
61+ desc : 'upload manifests' ,
62+ } ,
63+ )
3064
31- const supportedFilesCResult = sockSdk
32- ? await fetchSupportedScanFileNames ( )
33- : undefined
65+ if ( ! uploadCResult . ok ) {
66+ return uploadCResult
67+ }
3468
35- if ( supportedFilesCResult ) {
36- lastCResult = supportedFilesCResult
69+ const tarHash : string = ( uploadCResult as any ) . data . tarHash
70+ if ( ! tarHash ) {
71+ spinner ?. stop ( )
72+ return {
73+ ok : false ,
74+ message :
75+ 'No tar hash returned from Socket API upload-manifest-files endpoint' ,
76+ data : uploadCResult . data ,
77+ }
3778 }
3879
39- const supportedFiles = supportedFilesCResult ?. ok
40- ? supportedFilesCResult . data
41- : undefined
80+ const isAll =
81+ ghsas . length === 1 && ( ghsas [ 0 ] === 'all' || ghsas [ 0 ] === 'auto' )
82+
83+ const shouldOpenPrs = fixEnv . isCi && fixEnv . repoInfo
4284
43- const packagePaths = supportedFiles
44- ? await getPackageFilesForScan ( [ '.' ] , supportedFiles ! , {
85+ if ( ! shouldOpenPrs ) {
86+ const ids = isAll ? [ 'all' ] : ghsas . slice ( 0 , limit )
87+ if ( ! ids . length ) {
88+ spinner ?. stop ( )
89+ return { ok : true , data : { fixed : false } }
90+ }
91+ const fixCResult = await spawnCoana (
92+ [
93+ 'compute-fixes-and-upgrade-purls' ,
4594 cwd ,
46- } )
47- : [ ]
95+ '--manifests-tar-hash' ,
96+ tarHash ,
97+ '--apply-fixes-to' ,
98+ ...( isAll ? [ 'all' ] : ghsas ) ,
99+ ...fixConfig . unknownFlags ,
100+ ] ,
101+ fixConfig . orgSlug ,
102+ { cwd, spinner } ,
103+ )
104+ spinner ?. stop ( )
105+ return fixCResult . ok ? { ok : true , data : { fixed : true } } : fixCResult
106+ }
48107
49- const uploadCResult = sockSdk
50- ? await handleApiCall ( sockSdk ?. uploadManifestFiles ( orgSlug , packagePaths ) , {
51- desc : 'upload manifests' ,
52- } )
53- : undefined
108+ let ids : string [ ] | undefined
109+ if ( isAll ) {
110+ const foundCResult = await spawnCoana (
111+ [
112+ 'compute-fixes-and-upgrade-purls' ,
113+ cwd ,
114+ '--manifests-tar-hash' ,
115+ tarHash ,
116+ ...fixConfig . unknownFlags ,
117+ ] ,
118+ fixConfig . orgSlug ,
119+ { cwd, spinner } ,
120+ )
121+ if ( foundCResult . ok ) {
122+ const foundIds = cmdFlagValueToArray (
123+ / (?< = V u l n e r a b i l i t i e s f o u n d : ) .* / . exec ( foundCResult . data ) ,
124+ )
125+ ids = foundIds . slice ( 0 , limit )
126+ }
127+ } else {
128+ ids = ghsas . slice ( 0 , limit )
129+ }
54130
55- if ( uploadCResult ) {
56- lastCResult = uploadCResult
131+ if ( ! ids ?. length ) {
132+ debugFn ( 'notice' , 'miss: no GHSA IDs to process' )
57133 }
58134
59- const tarHash = uploadCResult ?. ok ? ( uploadCResult as any ) . data . tarHash : ''
135+ if ( ! fixEnv . repoInfo ) {
136+ debugFn ( 'notice' , 'miss: no repo info detected' )
137+ }
60138
61- if ( ! tarHash ) {
139+ if ( ! ids ?. length || ! fixEnv . repoInfo ) {
62140 spinner ?. stop ( )
63- return lastCResult as CResult < any >
141+ return { ok : true , data : { fixed : false } }
64142 }
65143
66- const isAllOrAuto =
67- ghsas . length === 1 && ( ghsas [ 0 ] === 'all' || ghsas [ 0 ] === 'auto' )
144+ const scanBaseNames = new Set ( scanFilepaths . map ( p => path . basename ( p ) ) )
68145
69- const ids = isAllOrAuto ? [ 'all' ] : ghsas
146+ let count = 0
147+ let overallFixed = false
148+
149+ // Process each GHSA ID individually, similar to npm-fix/pnpm-fix.
150+ ghsaLoop: for ( let i = 0 , { length } = ids ; i < length ; i += 1 ) {
151+ const id = ids [ i ] !
152+ debugFn ( 'notice' , `Processing GHSA ID: ${ id } ` )
153+
154+ // Apply fix for single GHSA ID.
155+ // eslint-disable-next-line no-await-in-loop
156+ const fixCResult = await spawnCoana (
157+ [
158+ 'compute-fixes-and-upgrade-purls' ,
159+ cwd ,
160+ '--manifests-tar-hash' ,
161+ tarHash ,
162+ '--apply-fixes-to' ,
163+ id ,
164+ ...fixConfig . unknownFlags ,
165+ ] ,
166+ fixConfig . orgSlug ,
167+ { cwd, spinner } ,
168+ )
70169
71- const fixCResult = ids . length
72- ? await spawnCoana (
73- [
74- 'compute-fixes-and-upgrade-purls' ,
170+ if ( ! fixCResult . ok ) {
171+ logger . error (
172+ `Update failed for ${ id } : ${ fixCResult . message || 'Unknown error' } ` ,
173+ )
174+ continue ghsaLoop
175+ }
176+
177+ // Check for modified files after applying the fix.
178+ // eslint-disable-next-line no-await-in-loop
179+ const unstagedCResult = await gitUnstagedModifiedFiles ( cwd )
180+ const modifiedFiles = unstagedCResult . ok
181+ ? unstagedCResult . data . filter ( relPath =>
182+ scanBaseNames . has ( path . basename ( relPath ) ) ,
183+ )
184+ : [ ]
185+
186+ if ( ! modifiedFiles . length ) {
187+ debugFn ( 'notice' , `skip: no changes for ${ id } ` )
188+ continue ghsaLoop
189+ }
190+
191+ overallFixed = true
192+
193+ // Create PR if in CI environment
194+ try {
195+ const branch = `socket/coana-fix/${ id } `
196+
197+ // Check if branch already exists
198+ // eslint-disable-next-line no-await-in-loop
199+ if ( await gitRemoteBranchExists ( branch , cwd ) ) {
200+ debugFn ( 'notice' , `skip: remote branch "${ branch } " exists` )
201+ continue ghsaLoop
202+ }
203+
204+ debugFn ( 'notice' , `pr: creating for ${ id } ` )
205+
206+ const pushed =
207+ // eslint-disable-next-line no-await-in-loop
208+ ( await gitCreateBranch ( branch , cwd ) ) &&
209+ // eslint-disable-next-line no-await-in-loop
210+ ( await gitCheckoutBranch ( branch , cwd ) ) &&
211+ // eslint-disable-next-line no-await-in-loop
212+ ( await gitCommit (
213+ `fix: Apply Coana security fix for ${ id } ` ,
214+ modifiedFiles ,
215+ {
216+ cwd,
217+ email : fixEnv . gitEmail ,
218+ user : fixEnv . gitUser ,
219+ } ,
220+ ) ) &&
221+ // eslint-disable-next-line no-await-in-loop
222+ ( await gitPushBranch ( branch , cwd ) )
223+
224+ if ( ! pushed ) {
225+ logger . warn ( `Push failed for ${ id } , skipping PR creation.` )
226+ // eslint-disable-next-line no-await-in-loop
227+ await gitResetAndClean ( fixEnv . baseBranch , cwd )
228+ // eslint-disable-next-line no-await-in-loop
229+ await gitCheckoutBranch ( fixEnv . baseBranch , cwd )
230+ // eslint-disable-next-line no-await-in-loop
231+ await gitDeleteBranch ( branch , cwd )
232+ continue ghsaLoop
233+ }
234+
235+ // Set up git remote.
236+ // eslint-disable-next-line no-await-in-loop
237+ await setGitRemoteGithubRepoUrl (
238+ fixEnv . repoInfo . owner ,
239+ fixEnv . repoInfo . repo ,
240+ fixEnv . githubToken ! ,
241+ cwd ,
242+ )
243+
244+ // eslint-disable-next-line no-await-in-loop
245+ const prResponse = await openCoanaPr (
246+ fixEnv . repoInfo . owner ,
247+ fixEnv . repoInfo . repo ,
248+ branch ,
249+ // Single GHSA ID.
250+ [ id ] ,
251+ {
252+ baseBranch : fixEnv . baseBranch ,
75253 cwd,
76- '--manifests-tar-hash' ,
77- tarHash ,
78- '--apply-fixes-to' ,
79- ...ids ,
80- ...fixConfig . unknownFlags ,
81- ] ,
82- fixConfig . orgSlug ,
83- { cwd, spinner } ,
254+ } ,
84255 )
85- : undefined
86256
87- if ( fixCResult ) {
88- lastCResult = fixCResult
257+ if ( prResponse ) {
258+ const { data } = prResponse
259+ const prRef = `PR #${ data . number } `
260+ logger . success ( `Opened ${ prRef } for ${ id } .` )
261+
262+ if ( autoMerge ) {
263+ logger . indent ( )
264+ spinner ?. indent ( )
265+ // eslint-disable-next-line no-await-in-loop
266+ const { details, enabled } = await enablePrAutoMerge ( data )
267+ if ( enabled ) {
268+ logger . info ( `Auto-merge enabled for ${ prRef } .` )
269+ } else {
270+ const message = `Failed to enable auto-merge for ${ prRef } ${
271+ details ? `:\n${ details . map ( d => ` - ${ d } ` ) . join ( '\n' ) } ` : '.'
272+ } `
273+ logger . error ( message )
274+ }
275+ logger . dedent ( )
276+ spinner ?. dedent ( )
277+ }
278+ }
279+
280+ // Reset back to base branch for next iteration.
281+ // eslint-disable-next-line no-await-in-loop
282+ await gitResetAndClean ( branch , cwd )
283+ // eslint-disable-next-line no-await-in-loop
284+ await gitCheckoutBranch ( fixEnv . baseBranch , cwd )
285+ } catch ( e ) {
286+ logger . warn (
287+ `Unexpected condition: Push failed for ${ id } , skipping PR creation.` ,
288+ )
289+ debugDir ( 'inspect' , { error : e } )
290+ // eslint-disable-next-line no-await-in-loop
291+ await gitResetAndClean ( fixEnv . baseBranch , cwd )
292+ // eslint-disable-next-line no-await-in-loop
293+ await gitCheckoutBranch ( fixEnv . baseBranch , cwd )
294+ }
295+
296+ count += 1
297+ debugFn ( 'notice' , `Processed ${ count } /${ Math . min ( limit , ids . length ) } fixes` )
298+ if ( count >= limit ) {
299+ break ghsaLoop
300+ }
89301 }
90302
91303 spinner ?. stop ( )
92304
93- debugDir ( 'inspect' , { lastCResult } )
94-
95- return lastCResult . ok
96- ? {
97- ok : true ,
98- data : { fixed : true } ,
99- }
100- : ( lastCResult as CResult < any > )
305+ return {
306+ ok : true ,
307+ data : { fixed : overallFixed } ,
308+ }
101309}
0 commit comments