Skip to content

Commit 1352c55

Browse files
fix: acceptance tests for secrets test output [PS-357]
1 parent c6af07f commit 1352c55

1 file changed

Lines changed: 294 additions & 0 deletions

File tree

test/jest/acceptance/snyk-secrets/snyk-secrets-test-user-journey.spec.ts

Lines changed: 294 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { matchers } from 'jest-json-schema';
55
import { runSnykCLI } from '../../util/runSnykCLI';
66
import { EXIT_CODES } from '../../../../src/cli/exit-codes';
77
import { resolve } from 'path';
8+
import { randomUUID } from 'crypto';
89

910
expect.extend(matchers);
1011
jest.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+
57121
describe.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 = /Finding ID: [0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-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(/Ignored:\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(/Expiration:\s+[A-Z][a-z]+\s+\d{2},\s+\d{4}/);
304+
305+
// Validates the Reason field and spacing
306+
expect(stdout).toMatch(/Reason:\s+Test ignore reason metadata 0/);
307+
expect(stdout).toMatch(/Reason:\s+Test ignore reason metadata 1/);
308+
expect(stdout).toMatch(/Ignored on:\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-f0-9]{64}$/i;
361+
const slugRegex = /^[a-z0-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

Comments
 (0)