@@ -274,7 +274,10 @@ export const RUN_SPECS = {
274274 } ,
275275 trivy : {
276276 normalize : normalizeTrivyFindings ,
277- argv : ( rootDir ) => [ 'fs' , '--scanners' , 'vuln,misconfig,secret' , '--format' , 'json' , '--quiet' , rootDir ] ,
277+ // misconfig + secret only: dependency vulnerabilities are already covered by
278+ // OSV-Scanner, and dropping `vuln` avoids Trivy's large vulnerability-DB
279+ // download (which otherwise blows the run timeout on the first scan).
280+ argv : ( rootDir ) => [ 'fs' , '--scanners' , 'misconfig,secret' , '--format' , 'json' , '--quiet' , rootDir ] ,
278281 parsesStdout : true ,
279282 } ,
280283} ;
@@ -285,25 +288,43 @@ export const RUN_SPECS = {
285288 * checksum-verified tool path). Fail-open: any error yields available:false.
286289 *
287290 * @param {string } toolKey
288- * @param {{ rootDir: string, toolPath: string, exec: (path: string, argv: string[]) => Promise<{stdout: string, stderr?: string}> } } opts
291+ * @param {{ rootDir: string, toolPath: string, exec: (path: string, argv: string[]) => Promise<{stdout: string, stderr?: string}>, retryDelayMs?: number } } opts
289292 * @returns {Promise<{tool: string, available: boolean, findings: Array<object>, rawCount: number, error?: string}> }
290293 */
291294export async function runSecurityTool ( toolKey , opts ) {
292295 const spec = RUN_SPECS [ toolKey ] ;
293296 if ( ! spec ) return { tool : toolKey , available : false , findings : [ ] , rawCount : 0 , error : `unknown tool ${ toolKey } ` } ;
294- try {
297+
298+ const runOnce = async ( ) => {
295299 const { stdout } = await opts . exec ( opts . toolPath , spec . argv ( opts . rootDir ) ) ;
296300 const parsed = JSON . parse ( stdout || ( toolKey === 'gitleaks' ? '[]' : '{}' ) ) ;
297301 const findings = spec . normalize ( parsed ) ;
298302 return { tool : toolKey , available : true , findings, rawCount : findings . length } ;
299- } catch ( err ) {
300- return {
301- tool : toolKey ,
302- available : false ,
303- findings : [ ] ,
304- rawCount : 0 ,
305- error : err && err . message ? err . message : String ( err ) ,
306- } ;
303+ } ;
304+
305+ try {
306+ return await runOnce ( ) ;
307+ } catch ( firstErr ) {
308+ const message = firstErr && firstErr . message ? firstErr . message : String ( firstErr ) ;
309+ // Never retry a timeout (that would double a slow scan). DO retry a transient
310+ // failure once — a freshly provisioned binary can be briefly locked on Windows
311+ // (e.g. by antivirus) the instant after it is downloaded.
312+ const isTimeout = Boolean ( firstErr && ( firstErr . killed || / t i m e d ? \s * o u t | E T I M E D O U T / i. test ( message ) ) ) ;
313+ if ( isTimeout ) {
314+ return { tool : toolKey , available : false , findings : [ ] , rawCount : 0 , error : message } ;
315+ }
316+ await new Promise ( ( resolve ) => setTimeout ( resolve , opts . retryDelayMs == null ? 400 : opts . retryDelayMs ) ) ;
317+ try {
318+ return await runOnce ( ) ;
319+ } catch ( secondErr ) {
320+ return {
321+ tool : toolKey ,
322+ available : false ,
323+ findings : [ ] ,
324+ rawCount : 0 ,
325+ error : secondErr && secondErr . message ? secondErr . message : String ( secondErr ) ,
326+ } ;
327+ }
307328 }
308329}
309330
0 commit comments