@@ -5,6 +5,7 @@ import { matchers } from 'jest-json-schema';
55import { runSnykCLI } from '../../util/runSnykCLI' ;
66import { EXIT_CODES } from '../../../../src/cli/exit-codes' ;
77import { resolve } from 'path' ;
8+ import { randomUUID } from 'crypto' ;
89
910expect . extend ( matchers ) ;
1011jest . setTimeout ( 1000 * 180 ) ;
@@ -54,6 +55,69 @@ afterAll(() => {
5455 }
5556} ) ;
5657
58+
59+ /**
60+ * Sets up an isolated environment for testing the 'ignore' functionality.
61+ * * Why this is necessary:
62+ * - Generates unique secret identities within a dedicated temporary folder.
63+ * - Isolates local state mutations (like .snyk file creation).
64+ * - Prevents race conditions during concurrent test execution, guaranteeing
65+ * zero side-effects on other acceptance tests.
66+ */
67+ const setupIsolatedIgnoreEnv = async ( basePath : string ) => {
68+ const uuid = randomUUID ( ) ;
69+ const testDir = `${ basePath } /ignores_test_${ uuid } ` ;
70+
71+ // Calculate an expiry date 15 minutes from now in YYYY-MM-DDThh:mm:ss.fffZ format
72+ const expiryDate = new Date ( Date . now ( ) + 15 * 60000 ) . toISOString ( ) ;
73+
74+ const cleanup = ( ) => {
75+ if ( existsSync ( testDir ) ) {
76+ try {
77+ execSync ( `rm -rf "${ testDir } "` , { stdio : 'pipe' } ) ;
78+ } catch ( err : any ) {
79+ console . warn ( `Failed to cleanup isolated ignore directory:` , err . message ) ;
80+ }
81+ }
82+ } ;
83+
84+ try {
85+ execSync ( `mkdir -p "${ testDir } "` ) ;
86+
87+ // Copy the same file twice to trigger the same rule ID for multiple locations in SARIF validation
88+ execSync (
89+ `cp "${ basePath } /semgrep-rules-examples/detected-sendgrid-api-key.txt" "${ testDir } /sendgrid-keys_1_${ uuid } .txt"` ,
90+ { stdio : 'pipe' }
91+ ) ;
92+ execSync (
93+ `cp "${ basePath } /semgrep-rules-examples/detected-sendgrid-api-key.txt" "${ testDir } /sendgrid-keys_2_${ uuid } .txt"` ,
94+ { stdio : 'pipe' }
95+ ) ;
96+
97+ // Run a base JSON scan to extract the exact Issue/Rule IDs for these files
98+ const { stdout : jsonStdout } = await runSnykCLI ( `secrets test "${ testDir } " --json` , { env } ) ;
99+ const jsonOutput = JSON . parse ( jsonStdout ) ;
100+ const results = jsonOutput . runs [ 0 ] . results || [ ] ;
101+
102+ const ruleIds = [ ...new Set ( results . map ( ( r : any ) => r . ruleId ) ) ] ;
103+ const issuesToIgnore = ruleIds . slice ( 0 , 2 ) ;
104+
105+ // Ignore the target issues
106+ for ( const [ index , issueId ] of issuesToIgnore . entries ( ) ) {
107+ const reason = `Test ignore reason metadata ${ index } ` ;
108+ await runSnykCLI (
109+ `ignore --id=${ issueId } --expiry=${ expiryDate } --reason="${ reason } "` ,
110+ { env, cwd : testDir }
111+ ) ;
112+ }
113+
114+ return { testDir, cleanup } ;
115+ } catch ( error ) {
116+ cleanup ( ) ;
117+ throw error ;
118+ }
119+ } ;
120+
57121describe . skip ( 'snyk secrets test' , ( ) => {
58122 describe ( 'output formats' , ( ) => {
59123 it ( 'should display human-readable output by default' , async ( ) => {
@@ -170,6 +234,236 @@ describe.skip('snyk secrets test', () => {
170234 } ) ;
171235 } ) ;
172236
237+ describe ( 'Human-readable output validation' , ( ) => {
238+ it ( 'should generate properly formatted human-readable output and map Finding IDs correctly' , async ( ) => {
239+ const { code, stdout, stderr } = await runSnykCLI (
240+ `secrets test ${ TEMP_LOCAL_PATH } /${ TEST_DIR } ` ,
241+ { env } ,
242+ ) ;
243+
244+ expect ( stderr ) . toBe ( '' ) ;
245+ expect ( code ) . toBe ( EXIT_CODES . VULNS_FOUND ) ;
246+
247+ expect ( stdout ) . toContain ( 'Open Secrets issues:' ) ;
248+ expect ( stdout ) . toContain ( 'Test Summary' ) ;
249+ expect ( stdout ) . toContain ( 'Total secrets issues:' ) ;
250+
251+ // Validates: Finding ID is mapped correctly to a UUID string
252+ const uuidRegex = / F i n d i n g I D : [ 0 - 9 a - f ] { 8 } - [ 0 - 9 a - f ] { 4 } - [ 1 - 5 ] [ 0 - 9 a - f ] { 3 } - [ 8 9 a b ] [ 0 - 9 a - f ] { 3 } - [ 0 - 9 a - f ] { 12 } / i;
253+ expect ( stdout ) . toMatch ( uuidRegex ) ;
254+ } ) ;
255+
256+ it ( 'should omit Finding IDs that start with UNDEFINED' , async ( ) => {
257+ // Create a path outside of the repo to remove source used to generate secret identities
258+ const NO_GIT_DIR = `${ TEMP_LOCAL_PATH } _nogit` ;
259+
260+ try {
261+ // Copy just the test files,
262+ execSync ( `mkdir -p ${ NO_GIT_DIR } ` ) ;
263+ execSync ( `cp -r ${ TEMP_LOCAL_PATH } /${ TEST_DIR } /* ${ NO_GIT_DIR } /` , { stdio : 'pipe' } ) ;
264+
265+ const { code, stdout, stderr } = await runSnykCLI (
266+ `secrets test ${ NO_GIT_DIR } ` ,
267+ { env } ,
268+ ) ;
269+
270+ expect ( stderr ) . toBe ( '' ) ;
271+ expect ( code ) . toBe ( EXIT_CODES . VULNS_FOUND ) ;
272+
273+ // Finding ID should not be included when it starts with UNDEFINED
274+ expect ( stdout ) . not . toContain ( 'Finding ID: UNDEFINED' ) ;
275+ } finally {
276+ try {
277+ execSync ( `rm -rf ${ NO_GIT_DIR } ` , { stdio : 'pipe' } ) ;
278+ } catch ( err : any ) {
279+ console . warn ( `Failed to cleanup non-git test directory:` , err . message ) ;
280+ }
281+ }
282+ } ) ;
283+
284+ it ( 'should correctly render multiple ignores and their metadata in the output' , async ( ) => {
285+ const { testDir, cleanup } = await setupIsolatedIgnoreEnv ( TEMP_LOCAL_PATH ) ;
286+
287+ try {
288+ // Get human-readable with the ignores included
289+ const { stdout, stderr, code } = await runSnykCLI (
290+ `secrets test ${ testDir } --include-ignores` ,
291+ { env, cwd : testDir }
292+ ) ;
293+
294+ expect ( stderr ) . toBe ( '' ) ;
295+ expect ( code ) . toBe ( EXIT_CODES . VULNS_FOUND ) ;
296+
297+ // Multiple ignores are rendered properly
298+ expect ( stdout ) . toMatch ( / I g n o r e d : \s * [ 2 - 9 ] / ) ;
299+ expect ( stdout ) . toContain ( '! [IGNORED]' ) ;
300+
301+ // Validate ignores metadata is mapped and rendered correctly
302+ // Validates Expiration format
303+ expect ( stdout ) . toMatch ( / E x p i r a t i o n : \s + [ A - Z ] [ a - z ] + \s + \d { 2 } , \s + \d { 4 } / ) ;
304+
305+ // Validates the Reason field and spacing
306+ expect ( stdout ) . toMatch ( / R e a s o n : \s + T e s t i g n o r e r e a s o n m e t a d a t a 0 / ) ;
307+ expect ( stdout ) . toMatch ( / R e a s o n : \s + T e s t i g n o r e r e a s o n m e t a d a t a 1 / ) ;
308+ expect ( stdout ) . toMatch ( / I g n o r e d o n : \s + [ A - Z ] [ a - z ] + \s + \d { 2 } , \s + \d { 4 } / ) ;
309+ } finally {
310+ cleanup ( ) ;
311+ }
312+ } ) ;
313+ } ) ;
314+
315+ describe ( 'JSON output payload validation' , ( ) => {
316+ it ( 'should return a valid SARIF when json flag is used' , async ( ) => {
317+ const { code, stdout, stderr } = await runSnykCLI (
318+ `secrets test ${ TEMP_LOCAL_PATH } /${ TEST_DIR } --json` ,
319+ { env } ,
320+ ) ;
321+
322+ expect ( stderr ) . toBe ( '' ) ;
323+ expect ( code ) . toBe ( EXIT_CODES . VULNS_FOUND ) ;
324+
325+ const output = JSON . parse ( stdout ) ;
326+
327+ // Basic SARIF schema requirements
328+ expect ( output ) . toHaveProperty ( 'version' ) ;
329+ expect ( typeof output . version ) . toBe ( 'string' ) ;
330+ expect ( Array . isArray ( output . runs ) ) . toBe ( true ) ;
331+ expect ( output . runs . length ) . toBeGreaterThan ( 0 ) ;
332+
333+ const run = output . runs [ 0 ] ;
334+
335+ expect ( run . tool . driver . name ) . toBe ( 'Snyk Secrets' ) ;
336+
337+ // Results array exists and is populated
338+ expect ( Array . isArray ( run . results ) ) . toBe ( true ) ;
339+ expect ( run . results . length ) . toBeGreaterThan ( 0 ) ;
340+
341+ // Ensure the first result has the expected ruleId mapping
342+ expect ( run . results [ 0 ] ) . toHaveProperty ( 'ruleId' ) ;
343+ } ) ;
344+ } ) ;
345+
346+ describe ( 'SARIF output payload validation' , ( ) => {
347+ it ( 'should generate an enriched SARIF payload with ignores' , async ( ) => {
348+ const { testDir, cleanup } = await setupIsolatedIgnoreEnv ( TEMP_LOCAL_PATH ) ;
349+
350+ try {
351+ const { code, stdout, stderr } = await runSnykCLI (
352+ `secrets test "${ testDir } " --include-ignores --sarif` ,
353+ { env, cwd : testDir } ,
354+ ) ;
355+
356+ expect ( stderr ) . toBe ( '' ) ;
357+ expect ( code ) . toBe ( EXIT_CODES . VULNS_FOUND ) ;
358+
359+ const sarifOutput = JSON . parse ( stdout ) ;
360+ const fingerprintRegex = / ^ [ a - f 0 - 9 ] { 64 } $ / i;
361+ const slugRegex = / ^ [ a - z 0 - 9 - ] + $ / ;
362+
363+ // Only one run is performed
364+ const run = sarifOutput . runs [ 0 ] ;
365+
366+ expect ( run . tool . driver . name ) . toBe ( 'Snyk Secrets' ) ;
367+
368+ const rules = run . tool . driver . rules || [ ] ;
369+ const ruleIds = rules . map ( ( rule : any ) => rule . id ) ;
370+ const uniqueRuleIds = new Set ( ruleIds ) ;
371+
372+ // Rules should only be included once in the SARIF, and not multiple times
373+ expect ( ruleIds . length ) . toBe ( uniqueRuleIds . size ) ;
374+
375+ rules . forEach ( ( rule : any ) => {
376+ expect ( rule . id ) . toMatch ( slugRegex ) ;
377+
378+ // Rules should have name
379+ expect ( rule ) . toHaveProperty ( 'name' ) ;
380+
381+ // Validates: the properties from the rules include the severity
382+ expect ( rule . properties ) . toBeDefined ( ) ;
383+ expect ( rule . properties ) . toHaveProperty ( 'severity' ) ;
384+
385+ // General structural checks
386+ expect ( rule ) . toHaveProperty ( 'shortDescription.text' ) ;
387+ } ) ;
388+
389+ let foundMultipleLocations = false ;
390+ const results = run . results || [ ] ;
391+
392+ results . forEach ( ( result : any ) => {
393+ expect ( result . ruleId ) . toMatch ( slugRegex ) ;
394+
395+ // Validates: fingerprint is included in the result
396+ expect ( result ) . toHaveProperty ( 'fingerprints' ) ;
397+ expect ( result . fingerprints ) . toHaveProperty ( 'fingerprint' ) ;
398+ expect ( result . fingerprints . fingerprint ) . toMatch ( fingerprintRegex ) ;
399+
400+ expect ( Array . isArray ( result . locations ) ) . toBe ( true ) ;
401+ expect ( result . locations . length ) . toBeGreaterThan ( 0 ) ;
402+
403+ // Tracks if we successfully grouped multiple locations into a single result
404+ if ( result . locations . length > 1 ) {
405+ foundMultipleLocations = true ;
406+ }
407+
408+ // Validate ignores metadata includes only these fields: status, justification, kind
409+ if ( result . suppressions && result . suppressions . length > 0 ) {
410+ result . suppressions . forEach ( ( suppression : any ) => {
411+ const suppressionKeys = Object . keys ( suppression ) . sort ( ) ;
412+ const expectedKeys = [ 'justification' , 'kind' , 'status' ] . sort ( ) ;
413+ expect ( suppressionKeys ) . toEqual ( expectedKeys ) ;
414+ } ) ;
415+ }
416+ } ) ;
417+
418+ expect ( foundMultipleLocations ) . toBe ( true ) ;
419+ } finally {
420+ cleanup ( ) ;
421+ }
422+ } ) ;
423+ it ( 'should ensure consistent secret identities regardless of the working directory' , async ( ) => {
424+ // Use existing directories from the repo tree to test different path depths
425+ // DIR_A is 1 level deep, DIR_C is 2 levels deep
426+ const DIR_A = `${ TEMP_LOCAL_PATH } /auth0` ;
427+ const DIR_C = `${ TEMP_LOCAL_PATH } /aws/valid` ;
428+
429+ const targetDir = 'semgrep-rules-examples' ;
430+
431+ // Run scan from DIR_A
432+ const { stdout : stdoutA , stderr : stderrA } = await runSnykCLI (
433+ `secrets test "../${ targetDir } " --sarif` ,
434+ { env, cwd : DIR_A }
435+ ) ;
436+ expect ( stderrA ) . toBe ( '' ) ;
437+
438+ // Run scan from DIR_C
439+ const { stdout : stdoutC , stderr : stderrC } = await runSnykCLI (
440+ `secrets test "../../${ targetDir } " --sarif` ,
441+ { env, cwd : DIR_C }
442+ ) ;
443+ expect ( stderrC ) . toBe ( '' ) ;
444+
445+ const sarifA = JSON . parse ( stdoutA ) ;
446+ const sarifC = JSON . parse ( stdoutC ) ;
447+
448+ const resultsA = sarifA . runs [ 0 ] . results || [ ] ;
449+ const resultsC = sarifC . runs [ 0 ] . results || [ ] ;
450+
451+ // Ensure we actually scanned and found the secrets
452+ expect ( resultsA . length ) . toBeGreaterThan ( 0 ) ;
453+ expect ( resultsA . length ) . toBe ( resultsC . length ) ;
454+
455+ // Helper to extract and sort fingerprints so order doesn't cause false failures
456+ const getFingerprints = ( results : any [ ] ) =>
457+ results . map ( ( r : any ) => r . fingerprints ?. fingerprint ) . sort ( ) ;
458+
459+ const fingerprintsA = getFingerprints ( resultsA ) ;
460+ const fingerprintsC = getFingerprints ( resultsC ) ;
461+
462+ // Identities must be exactly the same, as they are computed relative to the git root
463+ expect ( fingerprintsA ) . toEqual ( fingerprintsC ) ;
464+ } ) ;
465+ } ) ;
466+
173467 describe ( 'validation' , ( ) => {
174468 it ( 'should return an error for --report' , async ( ) => {
175469 const { code, stdout } = await runSnykCLI (
0 commit comments