Skip to content

Commit 56e7e95

Browse files
CatalinSnykdanskmt
authored andcommitted
feat: file filter doesn't exclude tracked files though .gitignore rules
1 parent c9683be commit 56e7e95

7 files changed

Lines changed: 662 additions & 8 deletions

File tree

cliv2-private/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -220,7 +220,7 @@ require (
220220
github.com/snyk/container-cli v0.0.0-20260213211631-cd2b2cf8f3ea // indirect
221221
github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62 // indirect
222222
github.com/snyk/error-catalog-golang-public v0.0.0-20260505112649-a5103d411663 // indirect
223-
github.com/snyk/go-application-framework v0.11.0 // indirect
223+
github.com/snyk/go-application-framework v0.11.1-0.20260805145855-e9b1d30707cd // indirect
224224
github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65 // indirect
225225
github.com/snyk/policy-engine v1.1.4 // indirect
226226
github.com/snyk/snyk-iac-capture v0.6.5 // indirect

cliv2-private/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -593,8 +593,8 @@ github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62 h1:kgZNQ5ztI4+n3
593593
github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62/go.mod h1:hTr91da/4ze2nk9q6ZW1BmfM2Z8rLUZSEZ3kK+6WGpc=
594594
github.com/snyk/error-catalog-golang-public v0.0.0-20260505112649-a5103d411663 h1:j2ZPhi78wKIHTiL9EFTNVXMIbsk56FVF2d5Sy1ZwSYk=
595595
github.com/snyk/error-catalog-golang-public v0.0.0-20260505112649-a5103d411663/go.mod h1:Ytttq7Pw4vOCu9NtRQaOeDU2dhBYUyNBe6kX4+nIIQ4=
596-
github.com/snyk/go-application-framework v0.11.0 h1:lOZhYO8JmzMoZKQbCb/jHMnrB/N3TvX8Z6oE/L9OQ7o=
597-
github.com/snyk/go-application-framework v0.11.0/go.mod h1:9GV/CTAhM8PT9MbxwYt/Za7tKDtw/Wuq6SyCu1XFzvk=
596+
github.com/snyk/go-application-framework v0.11.1-0.20260805145855-e9b1d30707cd h1:kQGGS+puiM17wD7QM0J2MCJIZGWMz/eDM4oLOhzdZGk=
597+
github.com/snyk/go-application-framework v0.11.1-0.20260805145855-e9b1d30707cd/go.mod h1:UbchJtcavs3z+C7oQttV8hv14LXJ5c7BjEmEvplu0Yg=
598598
github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65 h1:CEQuYv0Go6MEyRCD3YjLYM2u3Oxkx8GpCpFBd4rUTUk=
599599
github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65/go.mod h1:88KbbvGYlmLgee4OcQ19yr0bNpXpOr2kciOthaSzCAg=
600600
github.com/snyk/policy-engine v1.1.4 h1:0XpaMpl7ixSk4+dlpHYg2iKEBuv+5Ci+QIcbsmhktao=

cliv2/go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ require (
2222
github.com/snyk/code-client-go v1.31.3
2323
github.com/snyk/container-cli v0.0.0-20260213211631-cd2b2cf8f3ea
2424
github.com/snyk/error-catalog-golang-public v0.0.0-20260505112649-a5103d411663
25-
github.com/snyk/go-application-framework v0.11.0
25+
github.com/snyk/go-application-framework v0.11.1-0.20260805145855-e9b1d30707cd
2626
github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65
2727
github.com/snyk/snyk-iac-capture v0.6.5
2828
github.com/snyk/snyk-ls v0.0.0-20260804092303-865b318b1c8f

cliv2/go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -547,8 +547,8 @@ github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62 h1:kgZNQ5ztI4+n3
547547
github.com/snyk/dep-graph/go v0.0.0-20260127160647-c836da762c62/go.mod h1:hTr91da/4ze2nk9q6ZW1BmfM2Z8rLUZSEZ3kK+6WGpc=
548548
github.com/snyk/error-catalog-golang-public v0.0.0-20260505112649-a5103d411663 h1:j2ZPhi78wKIHTiL9EFTNVXMIbsk56FVF2d5Sy1ZwSYk=
549549
github.com/snyk/error-catalog-golang-public v0.0.0-20260505112649-a5103d411663/go.mod h1:Ytttq7Pw4vOCu9NtRQaOeDU2dhBYUyNBe6kX4+nIIQ4=
550-
github.com/snyk/go-application-framework v0.11.0 h1:lOZhYO8JmzMoZKQbCb/jHMnrB/N3TvX8Z6oE/L9OQ7o=
551-
github.com/snyk/go-application-framework v0.11.0/go.mod h1:9GV/CTAhM8PT9MbxwYt/Za7tKDtw/Wuq6SyCu1XFzvk=
550+
github.com/snyk/go-application-framework v0.11.1-0.20260805145855-e9b1d30707cd h1:kQGGS+puiM17wD7QM0J2MCJIZGWMz/eDM4oLOhzdZGk=
551+
github.com/snyk/go-application-framework v0.11.1-0.20260805145855-e9b1d30707cd/go.mod h1:UbchJtcavs3z+C7oQttV8hv14LXJ5c7BjEmEvplu0Yg=
552552
github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65 h1:CEQuYv0Go6MEyRCD3YjLYM2u3Oxkx8GpCpFBd4rUTUk=
553553
github.com/snyk/go-httpauth v0.0.0-20240307114523-1f5ea3f55c65/go.mod h1:88KbbvGYlmLgee4OcQ19yr0bNpXpOr2kciOthaSzCAg=
554554
github.com/snyk/policy-engine v1.1.4 h1:0XpaMpl7ixSk4+dlpHYg2iKEBuv+5Ci+QIcbsmhktao=

test/acceptance/fake-server.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -501,9 +501,13 @@ export const fakeServer = (basePath: string, snykToken: string): FakeServer => {
501501

502502
// Feature flag batch evaluation used by the Go binary's GAF layer
503503
// (config_utils.AddFeatureFlagToConfig → featureflaggateway.EvaluateFlags).
504-
// Request: POST /hidden/orgs/:orgId/feature_flags/evaluation
505504
// Body: { data: { attributes: { flags: ["flag-name", ...] } } }
506-
app.post('/hidden/orgs/:orgId/feature_flags/evaluation', (req, res) => {
505+
// GAF derives the URL from the API URL, so the /api prefix may or may not be present.
506+
const flagEvaluationPaths = [
507+
'/hidden/orgs/:orgId/feature_flags/evaluation',
508+
'/api/hidden/orgs/:orgId/feature_flags/evaluation',
509+
];
510+
app.post(flagEvaluationPaths, (req, res) => {
507511
const flags: string[] = req.body?.data?.attributes?.flags ?? [];
508512
// Maps batch evaluation API flag names to their GAF config keys.
509513
// The batch endpoint receives short API names; tests call setFeatureFlag
Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
// The fake server answers the batch flag evaluation endpoint, so the remote flag path is
2+
// testable without real backend access.
3+
import { runSnykCLI } from '../../util/runSnykCLI';
4+
import { runCommand } from '../../util/runCommand';
5+
import { fakeServer } from '../../../acceptance/fake-server';
6+
import { fakeDeepCodeServer } from '../../../acceptance/deepcode-fake-server';
7+
import { getServerPort } from '../../util/getServerPort';
8+
import * as fs from 'fs';
9+
import * as os from 'os';
10+
import { join } from 'path';
11+
12+
jest.setTimeout(1000 * 120);
13+
14+
const REMOTE_FLAG_NAME = 'clientFileFilterGitignore_TrackedFilesRollout';
15+
const FLAG_ENV = 'INTERNAL_SNYK_GITIGNORE_RESPECT_TRACKED_FILES_ENABLED';
16+
const PREVIEW_ENV = 'INTERNAL_PREVIEW_FEATURES_ENABLED';
17+
const EVALUATION_PATH = '/feature_flags/evaluation';
18+
19+
describe('snyk code test — tracked-file feature flag wiring', () => {
20+
let server: ReturnType<typeof fakeServer>;
21+
let deepCodeServer: ReturnType<typeof fakeDeepCodeServer>;
22+
let baseEnv: Record<string, string>;
23+
const port = getServerPort(process);
24+
const baseApi = '/api/v1';
25+
26+
beforeAll(async () => {
27+
deepCodeServer = fakeDeepCodeServer();
28+
await new Promise<void>((resolve) =>
29+
deepCodeServer.listen(() => resolve()),
30+
);
31+
server = fakeServer(baseApi, 'snykToken');
32+
await new Promise<void>((resolve) => server.listen(port, () => resolve()));
33+
34+
baseEnv = {
35+
...process.env,
36+
SNYK_API: `http://localhost:${port}${baseApi}`,
37+
SNYK_HOST: `http://localhost:${port}`,
38+
SNYK_TOKEN: '123456789',
39+
SNYK_CFG_ORG: '11111111-2222-3333-4444-555555555555',
40+
INTERNAL_SNYK_CODE_NATIVE_IMPLEMENTATION: 'true',
41+
// Preview/dev builds force the flag on (cliv2/pkg/core/workflows.go), bypassing
42+
// the remote flag. Without this, every assertion below is vacuous.
43+
[PREVIEW_ENV]: 'false',
44+
} as Record<string, string>;
45+
// The local override must be absent, otherwise the remote flag is never consulted.
46+
delete baseEnv[FLAG_ENV];
47+
});
48+
49+
afterAll(async () => {
50+
await new Promise<void>((resolve) => deepCodeServer.close(() => resolve()));
51+
await new Promise<void>((resolve) => server.close(() => resolve()));
52+
});
53+
54+
function configureServers(remoteFlagValue: boolean | undefined) {
55+
server.restore();
56+
deepCodeServer.restore();
57+
server.setOrgSetting('sast', true);
58+
server.setLocalCodeEngineConfiguration({
59+
enabled: true,
60+
allowCloudUpload: true,
61+
url: `http://localhost:${deepCodeServer.getPort()}`,
62+
});
63+
deepCodeServer.setFiltersResponse({ configFiles: [], extensions: ['.js'] });
64+
deepCodeServer.setSarifResponse({
65+
$schema: 'https://json.schemastore.org/sarif-2.1.0.json',
66+
version: '2.1.0',
67+
runs: [],
68+
});
69+
// Preview builds force this on too; enable it here so rule parsing matches
70+
// production rather than falling back to the legacy parser.
71+
server.setFeatureFlag('clientFileFilterGitignore_MetaCharFix', true);
72+
if (remoteFlagValue !== undefined) {
73+
server.setFeatureFlag(REMOTE_FLAG_NAME, remoteFlagValue);
74+
}
75+
}
76+
77+
/** A repo where tracked.js is both git-tracked and matched by .gitignore. */
78+
async function buildFixture(): Promise<string> {
79+
const root = fs.mkdtempSync(join(os.tmpdir(), 'snyk-ff-'));
80+
fs.writeFileSync(join(root, 'control.js'), 'const c = 0;\n');
81+
fs.writeFileSync(join(root, 'tracked.js'), 'const a = 1;\n');
82+
fs.writeFileSync(join(root, '.gitignore'), 'tracked.js\n');
83+
await runCommand('git', ['init'], { cwd: root });
84+
await runCommand('git', ['add', '-f', 'tracked.js'], { cwd: root });
85+
return root;
86+
}
87+
88+
function uploadedFiles(): string[] {
89+
const bundleRequest = deepCodeServer
90+
.getRequests()
91+
.find(
92+
(r) => r.method === 'POST' && (r.url as string).includes('/bundle'),
93+
);
94+
if (!bundleRequest) return [];
95+
const raw = Buffer.isBuffer(bundleRequest.body)
96+
? Buffer.from(bundleRequest.body.toString('utf8'), 'base64').toString(
97+
'utf8',
98+
)
99+
: JSON.stringify(bundleRequest.body);
100+
return Object.keys(JSON.parse(raw)).sort();
101+
}
102+
103+
/** The flags the CLI asked the backend to evaluate. */
104+
function evaluatedFlags(): string[] {
105+
return server
106+
.getRequests()
107+
.filter((r) => (r.url as string).includes(EVALUATION_PATH))
108+
.flatMap((r) => r.body?.data?.attributes?.flags ?? []);
109+
}
110+
111+
async function scan(opts: {
112+
remoteFlag?: boolean;
113+
envOverride?: boolean;
114+
previewFeatures?: boolean;
115+
}): Promise<{ files: string[]; flags: string[]; code: number }> {
116+
configureServers(opts.remoteFlag);
117+
const root = await buildFixture();
118+
const env = { ...baseEnv };
119+
if (opts.envOverride !== undefined) {
120+
env[FLAG_ENV] = String(opts.envOverride);
121+
}
122+
if (opts.previewFeatures) {
123+
env[PREVIEW_ENV] = 'true';
124+
}
125+
const { code } = await runSnykCLI(`code test ${root}`, { env });
126+
return { files: uploadedFiles(), flags: evaluatedFlags(), code };
127+
}
128+
129+
it('asks the backend to evaluate the tracked-files flag by its remote name', async () => {
130+
const { flags } = await scan({ remoteFlag: true });
131+
132+
// A wrong mapping here would leave a dead rollout switch that no behaviour test
133+
// would catch.
134+
expect(flags).toContain(REMOTE_FLAG_NAME);
135+
});
136+
137+
it('scans a tracked, gitignored file when the backend enables the flag', async () => {
138+
const { files } = await scan({ remoteFlag: true });
139+
140+
expect(files).toEqual(['control.js', 'tracked.js']);
141+
});
142+
143+
it('excludes a tracked, gitignored file when the backend disables the flag', async () => {
144+
const { files } = await scan({ remoteFlag: false });
145+
146+
expect(files).toEqual(['control.js']);
147+
});
148+
149+
it('defaults to the legacy behaviour when the backend does not know the flag', async () => {
150+
const { files } = await scan({});
151+
152+
expect(files).toEqual(['control.js']);
153+
});
154+
155+
it('a local override wins over the backend value', async () => {
156+
// Backend says on, local config says off.
157+
const off = await scan({ remoteFlag: true, envOverride: false });
158+
expect(off.files).toEqual(['control.js']);
159+
160+
// Backend says off, local config says on.
161+
const on = await scan({ remoteFlag: false, envOverride: true });
162+
expect(on.files).toEqual(['control.js', 'tracked.js']);
163+
});
164+
165+
describe('preview builds deliberately force the flag on', () => {
166+
// Intended behaviour. The consequence worth pinning: a preview binary cannot
167+
// validate the backend rollout switch.
168+
it('activates the feature even when the backend disables it', async () => {
169+
const { files } = await scan({
170+
remoteFlag: false,
171+
previewFeatures: true,
172+
});
173+
174+
expect(files).toEqual(['control.js', 'tracked.js']);
175+
});
176+
177+
it('does not even ask the backend to evaluate the flag', async () => {
178+
const { flags } = await scan({
179+
remoteFlag: false,
180+
previewFeatures: true,
181+
});
182+
183+
expect(flags).not.toContain(REMOTE_FLAG_NAME);
184+
});
185+
186+
it('still lets a local override turn the feature off', async () => {
187+
const { files, flags } = await scan({
188+
remoteFlag: true,
189+
previewFeatures: true,
190+
envOverride: false,
191+
});
192+
193+
expect(files).toEqual(['control.js']);
194+
// An override short-circuits the default-value function, so setting the key by
195+
// hand cannot be used to test the remote flag either.
196+
expect(flags).not.toContain(REMOTE_FLAG_NAME);
197+
});
198+
});
199+
});

0 commit comments

Comments
 (0)