1818 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
1919 */
2020import { existsSync } from 'node:fs' ;
21+ import { join } from 'node:path' ;
2122import { spawnProcess } from '../../../lib/process' ;
22- import type { SpawnResult } from '../../../lib/process' ;
23+ import type { SpawnOptions , SpawnResult } from '../../../lib/process' ;
2324import type { ResolvedAuth } from '../../../lib/auth-resolver' ;
2425import logger from '../../../lib/logger' ;
2526import { blank , error , print , success , text } from '../../../ui' ;
@@ -45,32 +46,71 @@ const BINARY_AUTH_TOKEN_ENV = 'SONAR_SECRETS_TOKEN';
4546const SCAN_TIMEOUT_MS = 30000 ;
4647const STDIN_READ_TIMEOUT_MS = 5000 ;
4748
49+ export const EXIT_CODE_SECRETS_FOUND = 51 ;
50+
51+ /**
52+ * Run sonar-secrets binary on the given files. Returns the full spawn result.
53+ * Kills the child process on timeout.
54+ */
55+ export async function runSecretsBinary (
56+ binaryPath : string ,
57+ files : string [ ] ,
58+ auth : ResolvedAuth ,
59+ ) : Promise < SpawnResult > {
60+ return spawnWithTimeout ( binaryPath , [ '--non-interactive' , ...files ] , {
61+ stdin : 'pipe' ,
62+ stdout : 'pipe' ,
63+ stderr : 'pipe' ,
64+ env : buildAuthEnv ( auth ) ,
65+ } ) ;
66+ }
67+
68+ function buildAuthEnv ( auth : ResolvedAuth ) : Record < string , string > {
69+ return { [ BINARY_AUTH_URL_ENV ] : auth . serverUrl , [ BINARY_AUTH_TOKEN_ENV ] : auth . token } ;
70+ }
71+
72+ async function spawnWithTimeout (
73+ binaryPath : string ,
74+ args : string [ ] ,
75+ options : SpawnOptions ,
76+ ) : Promise < SpawnResult > {
77+ let killChild : ( ( ) => void ) | undefined ;
78+ let timeoutId : ReturnType < typeof setTimeout > | undefined ;
79+ try {
80+ return await Promise . race ( [
81+ spawnProcess ( binaryPath , args , {
82+ ...options ,
83+ onSpawn : ( kill ) => {
84+ killChild = kill ;
85+ } ,
86+ } ) ,
87+ new Promise < never > ( ( _ , reject ) => {
88+ timeoutId = setTimeout ( ( ) => {
89+ killChild ?.( ) ;
90+ reject ( new Error ( `Scan timed out after ${ SCAN_TIMEOUT_MS } ms` ) ) ;
91+ } , SCAN_TIMEOUT_MS ) ;
92+ } ) ,
93+ ] ) ;
94+ } finally {
95+ clearTimeout ( timeoutId ) ;
96+ }
97+ }
98+
4899async function handleCheckCommand (
49100 options : AnalyzeSecretsOptions ,
50101 auth : ResolvedAuth ,
51102) : Promise < void > {
52103 validateScanOptions ( options ) ;
53104 const binaryPath = await installSecretsBinary ( ) ;
54- const { authUrl, authToken } = setupScanEnvironment ( binaryPath , auth ) ;
55105 const scanStartTime = Date . now ( ) ;
56106
57107 if ( options . stdin ) {
58- await performStdinScan ( binaryPath , authUrl , authToken , scanStartTime ) ;
108+ reportScanResult ( await runScanFromStdin ( binaryPath , auth ) , scanStartTime ) ;
59109 } else {
60- await performPathsScan ( binaryPath , options . paths ?? [ ] , authUrl , authToken , scanStartTime ) ;
110+ await performPathsScan ( binaryPath , options . paths ?? [ ] , auth , scanStartTime ) ;
61111 }
62112}
63113
64- interface ScanEnvironment {
65- binaryPath : string ;
66- authUrl ?: string ;
67- authToken ?: string ;
68- }
69-
70- function setupScanEnvironment ( binaryPath : string , auth : ResolvedAuth ) : ScanEnvironment {
71- return { binaryPath, authUrl : auth . serverUrl , authToken : auth . token } ;
72- }
73-
74114function validateScanOptions ( options : { paths ?: string [ ] ; stdin ?: boolean } ) : void {
75115 const hasPaths = ( options . paths ?. length ?? 0 ) > 0 ;
76116 if ( ! hasPaths && ! options . stdin ) {
@@ -82,28 +122,10 @@ function validateScanOptions(options: { paths?: string[]; stdin?: boolean }): vo
82122 }
83123}
84124
85- async function performStdinScan (
86- binaryPath : string ,
87- authUrl : string | undefined ,
88- authToken : string | undefined ,
89- scanStartTime : number ,
90- ) : Promise < void > {
91- const result = await runScanFromStdin ( binaryPath , authUrl , authToken ) ;
92- const scanDurationMs = Date . now ( ) - scanStartTime ;
93-
94- const exitCode = result . exitCode ?? 1 ;
95- if ( exitCode === 0 ) {
96- handleScanSuccess ( result , scanDurationMs ) ;
97- } else {
98- handleScanFailure ( result , scanDurationMs , exitCode ) ;
99- }
100- }
101-
102125async function performPathsScan (
103126 binaryPath : string ,
104127 paths : string [ ] ,
105- authUrl : string | undefined ,
106- authToken : string | undefined ,
128+ auth : ResolvedAuth ,
107129 scanStartTime : number ,
108130) : Promise < void > {
109131 if ( paths . length === 0 ) {
@@ -116,9 +138,12 @@ async function performPathsScan(
116138 }
117139 }
118140
119- const result = await runScan ( binaryPath , paths , authUrl , authToken ) ;
120- const scanDurationMs = Date . now ( ) - scanStartTime ;
141+ const result = await runSecretsBinary ( binaryPath , paths , auth ) ;
142+ reportScanResult ( result , scanStartTime ) ;
143+ }
121144
145+ function reportScanResult ( result : SpawnResult , scanStartTime : number ) : void {
146+ const scanDurationMs = Date . now ( ) - scanStartTime ;
122147 const exitCode = result . exitCode ?? 1 ;
123148 if ( exitCode === 0 ) {
124149 handleScanSuccess ( result , scanDurationMs ) ;
@@ -127,72 +152,21 @@ async function performPathsScan(
127152 }
128153}
129154
130- async function runScan (
131- binaryPath : string ,
132- paths : string [ ] ,
133- authUrl : string | undefined ,
134- authToken : string | undefined ,
135- ) : Promise < SpawnResult > {
136- let timeoutId : ReturnType < typeof setTimeout > | undefined ;
137- try {
138- return await Promise . race ( [
139- spawnProcess ( binaryPath , [ '--non-interactive' , ...paths ] , {
140- stdin : 'pipe' ,
141- stdout : 'pipe' ,
142- stderr : 'pipe' ,
143- env : {
144- ...( authUrl && authToken
145- ? { [ BINARY_AUTH_URL_ENV ] : authUrl , [ BINARY_AUTH_TOKEN_ENV ] : authToken }
146- : { } ) ,
147- } ,
148- } ) ,
149- new Promise < never > ( ( _resolve , reject ) => {
150- timeoutId = setTimeout ( ( ) => {
151- reject ( new Error ( `Scan timed out after ${ SCAN_TIMEOUT_MS } ms` ) ) ;
152- } , SCAN_TIMEOUT_MS ) ;
153- } ) ,
154- ] ) ;
155- } finally {
156- clearTimeout ( timeoutId ) ;
157- }
158- }
159-
160- async function runScanFromStdin (
161- binaryPath : string ,
162- authUrl : string | undefined ,
163- authToken : string | undefined ,
164- ) : Promise < SpawnResult > {
155+ async function runScanFromStdin ( binaryPath : string , auth : ResolvedAuth ) : Promise < SpawnResult > {
165156 const { writeFileSync, unlinkSync } = await import ( 'node:fs' ) ;
166157 const { tmpdir } = await import ( 'node:os' ) ;
167- const pathModule = await import ( 'node:path' ) ;
168- const pathJoin = ( ...args : string [ ] ) => pathModule . join ( ...args ) ;
169158
170159 const stdinData = await readStdin ( ) ;
160+ const tempFile = join ( tmpdir ( ) , `sonar-secrets-scan-${ Date . now ( ) } .tmp` ) ;
171161
172- const tempFile = pathJoin ( tmpdir ( ) , `sonar-secrets-scan-${ Date . now ( ) } .tmp` ) ;
173-
174- let timeoutId : ReturnType < typeof setTimeout > | undefined ;
162+ writeFileSync ( tempFile , stdinData ) ;
175163 try {
176- writeFileSync ( tempFile , stdinData ) ;
177-
178- return await Promise . race ( [
179- spawnProcess ( binaryPath , [ tempFile ] , {
180- stdout : 'pipe' ,
181- stderr : 'pipe' ,
182- env : {
183- ...( authUrl && authToken
184- ? { [ BINARY_AUTH_URL_ENV ] : authUrl , [ BINARY_AUTH_TOKEN_ENV ] : authToken }
185- : { } ) ,
186- } ,
187- } ) ,
188- new Promise < never > ( ( _resolve , reject ) => {
189- timeoutId = setTimeout ( ( ) => {
190- reject ( new Error ( `Scan timed out after ${ SCAN_TIMEOUT_MS } ms` ) ) ;
191- } , SCAN_TIMEOUT_MS ) ;
192- } ) ,
193- ] ) ;
164+ return await spawnWithTimeout ( binaryPath , [ tempFile ] , {
165+ stdout : 'pipe' ,
166+ stderr : 'pipe' ,
167+ env : buildAuthEnv ( auth ) ,
168+ } ) ;
194169 } finally {
195- clearTimeout ( timeoutId ) ;
196170 try {
197171 unlinkSync ( tempFile ) ;
198172 } catch {
@@ -228,21 +202,18 @@ async function readStdin(): Promise<string> {
228202}
229203
230204function handleScanSuccess ( result : { stdout : string } , scanDurationMs : number ) : void {
205+ blank ( ) ;
206+ success ( 'Scan completed successfully' ) ;
231207 try {
232208 const scanResult = JSON . parse ( result . stdout ) ;
233- blank ( ) ;
234- success ( 'Scan completed successfully' ) ;
235209 text ( ` Duration: ${ scanDurationMs } ms` ) ;
236210 displayScanResults ( scanResult ) ;
237- blank ( ) ;
238211 } catch ( parseError ) {
239212 logger . debug ( `Failed to parse JSON output: ${ ( parseError as Error ) . message } ` ) ;
240213 blank ( ) ;
241- success ( 'Scan completed successfully' ) ;
242- blank ( ) ;
243214 print ( result . stdout ) ;
244- blank ( ) ;
245215 }
216+ blank ( ) ;
246217}
247218
248219function displayScanResults ( scanResult : {
@@ -270,8 +241,6 @@ function displayScanResults(scanResult: {
270241 } ) ;
271242}
272243
273- const EXIT_CODE_SECRETS_FOUND = 51 ;
274-
275244function handleScanFailure (
276245 result : { exitCode : number | null ; stderr : string ; stdout : string } ,
277246 scanDurationMs : number ,
@@ -293,11 +262,7 @@ function handleScanFailure(
293262}
294263
295264function handleScanError ( err : unknown ) : void {
296- if ( err instanceof InvalidOptionError ) {
297- throw err ;
298- }
299-
300- if ( err instanceof CommandFailedError ) {
265+ if ( err instanceof InvalidOptionError || err instanceof CommandFailedError ) {
301266 throw err ;
302267 }
303268
0 commit comments