diff --git a/cliv2/go.mod b/cliv2/go.mod index 61e9c7a9c0..9dfc2b75e9 100644 --- a/cliv2/go.mod +++ b/cliv2/go.mod @@ -11,7 +11,7 @@ require ( github.com/pkg/errors v0.9.1 github.com/rs/zerolog v1.34.0 github.com/snyk/cli-extension-agent-scan v0.0.0-20260312152423-bc36193ecaa8 - github.com/snyk/cli-extension-ai-bom v0.0.0-20260303103300-ea9a5a717cbb + github.com/snyk/cli-extension-ai-bom v0.0.0-20260312142851-4a3ed1abe853 github.com/snyk/cli-extension-dep-graph v0.27.0 github.com/snyk/cli-extension-iac v0.0.0-20260206082514-00c443ccee80 github.com/snyk/cli-extension-iac-rules v0.0.0-20260206080712-9cbb5f95465d @@ -207,7 +207,7 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06 // indirect github.com/sagikazarmark/locafero v0.7.0 // indirect - github.com/samber/lo v1.52.0 // indirect + github.com/samber/lo v1.53.0 // indirect github.com/sergi/go-diff v1.4.0 // indirect github.com/shirou/gopsutil v3.21.11+incompatible // indirect github.com/sirupsen/logrus v1.9.3 // indirect diff --git a/cliv2/go.sum b/cliv2/go.sum index a00e694bf8..dba94ad699 100644 --- a/cliv2/go.sum +++ b/cliv2/go.sum @@ -520,8 +520,8 @@ github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06/go.mod h1:+e github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= github.com/sahilm/fuzzy v0.1.0/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= -github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw= -github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= +github.com/samber/lo v1.53.0 h1:t975lj2py4kJPQ6haz1QMgtId2gtmfktACxIXArw3HM= +github.com/samber/lo v1.53.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= @@ -533,8 +533,8 @@ github.com/skeema/knownhosts v1.3.2 h1:EDL9mgf4NzwMXCTfaxSD/o/a5fxDw/xL9nkU28Jjd github.com/skeema/knownhosts v1.3.2/go.mod h1:bEg3iQAuw+jyiw+484wwFJoKSLwcfd7fqRy+N0QTiow= github.com/snyk/cli-extension-agent-scan v0.0.0-20260312152423-bc36193ecaa8 h1:Ky+ZlFDH26kJ9QXeZPhQMG+1+M1EtNfa1BHBHL0zkh8= github.com/snyk/cli-extension-agent-scan v0.0.0-20260312152423-bc36193ecaa8/go.mod h1:+Znlgu2v7sOTNAVjsoldFjDZUIo8tpdKnFlMptZHzz0= -github.com/snyk/cli-extension-ai-bom v0.0.0-20260303103300-ea9a5a717cbb h1:ZgBgiMMtY8XK8WJZpmnlt08wKPMitDEU1TS99OK+k8A= -github.com/snyk/cli-extension-ai-bom v0.0.0-20260303103300-ea9a5a717cbb/go.mod h1:eIq61+KliPjLwhaAZT87FfeyfK/4mJaGP0wqyFtf8pQ= +github.com/snyk/cli-extension-ai-bom v0.0.0-20260312142851-4a3ed1abe853 h1:6CTQnacsK4/AXtijRhtwg/6TTGMCPwEL9ivUE7FxXn0= +github.com/snyk/cli-extension-ai-bom v0.0.0-20260312142851-4a3ed1abe853/go.mod h1:RnMP+tFTeKygfXSx7z+heyMZoOps67u5HFytjptHjuk= github.com/snyk/cli-extension-dep-graph v0.27.0 h1:yVy/QFeKdQUVL0PHZtPDSTk7icY2QrQPGKPjMFoCJwQ= github.com/snyk/cli-extension-dep-graph v0.27.0/go.mod h1:JQ37TXutjFa585Ocak1jfBRN6+QPppmFIlJ6+nrfgaY= github.com/snyk/cli-extension-iac v0.0.0-20260206082514-00c443ccee80 h1:JHbnSkgGc2oUejjzdWdeTghl0BZV7QamcRuyh7ornVo= diff --git a/test/acceptance/fake-server.ts b/test/acceptance/fake-server.ts index dcd0f7c2ac..35927f1c53 100644 --- a/test/acceptance/fake-server.ts +++ b/test/acceptance/fake-server.ts @@ -772,6 +772,35 @@ export const fakeServer = (basePath: string, snykToken: string): FakeServer => { }); }); + // AI-BOM CLI policy test (aibom test command) + app.post( + `/api/hidden/orgs/:orgId/ai_boms/cli_policy_test`, + (req: express.Request, res: express.Response) => { + res.status(200); + res.setHeader('Content-Type', 'application/vnd.api+json'); + res.send({ + jsonapi: { version: '1.0' }, + data: { + id: 'cli-policy-test-run-1', + type: 'test', + attributes: { + issues: [ + { + id: 'issue-1', + description: 'Disallowed model', + severity: 'high', + policy_id: 'pol-123', + state: 'open', + source: 'policy', + remediation_advice: 'Use an allowed model', + }, + ], + }, + }, + }); + }, + ); + // Unified Test API endpoints for uv acceptance tests const testJobId = 'aaaaaaaa-bbbb-cccc-dddd-000000000001'; const testId = 'aaaaaaaa-bbbb-cccc-dddd-000000000002'; diff --git a/test/jest/acceptance/snyk-aibom/aibom.spec.ts b/test/jest/acceptance/snyk-aibom/aibom.spec.ts index d142e85734..04a4aa3a75 100644 --- a/test/jest/acceptance/snyk-aibom/aibom.spec.ts +++ b/test/jest/acceptance/snyk-aibom/aibom.spec.ts @@ -6,6 +6,7 @@ import { } from '../../../acceptance/fake-server'; import { getServerPort } from '../../util/getServerPort'; import { resolve } from 'path'; +import * as os from 'os'; jest.setTimeout(1000 * 60 * 5); @@ -14,6 +15,8 @@ function aiBomRestEndpointRequests(requests: Request[]): string[] { for (const request of requests) { if (request.url.includes('/ai_boms/upload')) { res.push(`${request.method}:/ai_boms/upload`); + } else if (request.url.includes('cli_policy_test')) { + res.push(`${request.method}:/ai_boms/cli_policy_test`); } else if (request.url.includes('/ai_boms')) { res.push(`${request.method}:/ai_boms`); } else if (request.url.includes('/ai_bom_jobs')) { @@ -263,4 +266,81 @@ describe('snyk aibom (mocked servers only)', () => { expect(stdout).toContain('Authentication error (SNYK-0005)'); }); }); + + describe('snyk aibom test', () => { + test('`aibom test` runs policy test and returns open issues (exit code 1)', async () => { + expect(server.getRequests().length).toEqual(0); + const { code, stdout } = await runSnykCLI( + `aibom test ${pythonChatbotProject} --experimental`, + { + env, + }, + ); + expect(code).toEqual(1); + + const aiBomRequests = aiBomRestEndpointRequests(server.getRequests()); + expect(aiBomRequests).toContain('POST:/ai_boms/cli_policy_test'); + expect(aiBomRequests).toEqual([ + 'POST:/ai_boms', + 'POST:/upload_revisions', + 'POST:/upload_revisions/:uploadRevisionId/files', + 'PATCH:/upload_revisions/:uploadRevisionId', + 'POST:/ai_boms', + 'GET:/ai_bom_jobs', + 'GET:/ai_boms', + 'POST:/ai_boms/cli_policy_test', + ]); + + expect(stdout).toContain('AI BOM policy test'); + expect(stdout).toContain('Test summary'); + expect(stdout).toContain('Disallowed model'); + expect(stdout).toContain('Open'); + }); + + test('`aibom test` requires --experimental', async () => { + const { code, stdout } = await runSnykCLI( + `aibom test ${pythonChatbotProject}`, + { + env, + }, + ); + expect(code).toEqual(2); + expect(stdout).toContain('Command is experimental (SNYK-CLI-0015)'); + }); + + test('`aibom test` fails if api is unavailable', async () => { + expect(server.getRequests().length).toEqual(0); + server.setStatusCode(404); + const { code } = await runSnykCLI( + `aibom test ${pythonChatbotProject} --experimental`, + { + env, + }, + ); + expect(code).toEqual(2); + }); + + test('`aibom test` with --json-file-output writes results to file', async () => { + const outputPath = resolve( + os.tmpdir(), + `aibom-test-output-${Date.now()}.json`, + ); + const { code } = await runSnykCLI( + `aibom test ${pythonChatbotProject} --experimental --json-file-output=${outputPath}`, + { + env, + }, + ); + expect(code).toEqual(1); + const fs = await import('fs'); + const content = fs.readFileSync(outputPath, 'utf8'); + const result = JSON.parse(content); + expect(result.data).toBeDefined(); + expect(result.data.attributes.issues).toHaveLength(1); + expect(result.data.attributes.issues[0].description).toEqual( + 'Disallowed model', + ); + fs.unlinkSync(outputPath); + }); + }); });