Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion cliv2/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ require (
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-dep-graph v0.26.1
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
github.com/snyk/cli-extension-os-flows v0.0.0-20260306115903-79ae783267c1
Expand Down
4 changes: 2 additions & 2 deletions cliv2/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -535,8 +535,8 @@ github.com/snyk/cli-extension-agent-scan v0.0.0-20260312152423-bc36193ecaa8 h1:K
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-dep-graph v0.26.1 h1:aOov0GvCuvNbrkkk/CWggXFIIrFcBmRkNyE0yTMsTrc=
github.com/snyk/cli-extension-dep-graph v0.26.1/go.mod h1:JQ37TXutjFa585Ocak1jfBRN6+QPppmFIlJ6+nrfgaY=
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=
github.com/snyk/cli-extension-iac v0.0.0-20260206082514-00c443ccee80/go.mod h1:Ht5k+sWdi//fM2MjcmBMWjcJmr35iMvQpYlBWnUHL4I=
github.com/snyk/cli-extension-iac-rules v0.0.0-20260206080712-9cbb5f95465d h1:xkxHgZ+DT4hRiIEeAEv1JWLJRYV4MbAFvtEUpUkndPA=
Expand Down
1 change: 1 addition & 0 deletions src/lib/plugins/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface Options {
systemVersions?: object;
scanAllUnmanaged?: boolean;
showNpmScope?: boolean;
allProjects?: boolean;
}

export interface Plugin {
Expand Down
54 changes: 43 additions & 11 deletions src/lib/plugins/uv/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,18 @@ import { CustomError } from '../../errors';
import { execGoCommand, GoCommandResult } from '../../go-bridge';
import { truncateForLog } from '../../utils';
import * as types from '../types';
import { ScannedProjectCustom } from '../get-multi-plugin-result';

const debug = Debug('snyk:plugins:uv');
const UV_LOCKFILE_NAME = 'uv.lock';
const PYPROJECT_MANIFEST_NAME = 'pyproject.toml';

// JSON type returned from the Go plugin when using the --internal-uv-workspace-packages flag
interface WorkspacePackageResult {
depGraph: DepGraphData;
targetFile: string;
}

export async function inspect(
root: string,
targetFile: string,
Expand Down Expand Up @@ -41,16 +48,50 @@ export async function inspect(
if (strictOutOfSync !== undefined) {
args.push(`--strict-out-of-sync=${strictOutOfSync}`);
}
if (options?.allProjects) {
args.push('--internal-uv-workspace-packages');
}

const result = await execGoCommand(args, { cwd: root });

if (result.exitCode !== 0) {
throw createDepgraphError(extractErrorDetail(result));
}

let depGraphData: DepGraphData;
const resolvedTargetFile = getResolvedTargetFile(targetFile);

let scannedProjects: ScannedProjectCustom[];
try {
depGraphData = JSON.parse(result.stdout);
if (options?.allProjects) {
scannedProjects = result.stdout
.split('\n')
.filter((line) => line.trim().length > 0)
.map((line) => JSON.parse(line) as WorkspacePackageResult)
.map((workspacePkg) => ({
depGraph: createFromJSON(workspacePkg.depGraph),
targetFile: workspacePkg.targetFile,
packageManager: 'uv',
plugin: {
name: 'snyk-uv-plugin',
packageManager: 'uv',
targetFile: workspacePkg.targetFile,
},
}));
} else {
const depGraphData = JSON.parse(result.stdout) as DepGraphData;
scannedProjects = [
{
depGraph: createFromJSON(depGraphData),
targetFile: resolvedTargetFile,
packageManager: 'uv',
plugin: {
name: 'snyk-uv-plugin',
packageManager: 'uv',
targetFile: resolvedTargetFile,
},
},
];
}
} catch (error) {
const parseError = error instanceof Error ? error.message : String(error);
debug(
Expand All @@ -64,15 +105,6 @@ export async function inspect(
);
}

const resolvedTargetFile = getResolvedTargetFile(targetFile);

const scannedProjects = [
{
depGraph: createFromJSON(depGraphData),
targetFile: resolvedTargetFile,
},
];

return {
plugin: {
name: 'snyk-uv-plugin',
Expand Down
124 changes: 120 additions & 4 deletions src/lib/plugins/uv/uv.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ describe('uv plugin', () => {

expect(result.plugin).toEqual({
name: 'snyk-uv-plugin',
targetFile: 'pyproject.toml',
packageManager: 'uv',
targetFile: 'pyproject.toml',
});
expect(result.scannedProjects).toHaveLength(1);

Expand All @@ -111,7 +111,6 @@ describe('uv plugin', () => {
['depgraph', '--file=path/to/uv.lock', '--use-sbom-resolution', '--json'],
{ cwd: '.' },
);
expect(result.plugin.targetFile).toBe('path/to/pyproject.toml');
expect(result.scannedProjects[0].targetFile).toBe('path/to/pyproject.toml');
});

Expand Down Expand Up @@ -242,7 +241,7 @@ describe('uv plugin', () => {
const catalog = err.errorCatalog;
expect(catalog).toBeInstanceOf(CLI.GeneralCLIFailureError);
expect(catalog).toBeInstanceOf(ProblemError);
expect(catalog!.detail).toBe('some depgraph failure');
expect(catalog?.detail).toBe('some depgraph failure');
});

it('attaches an error catalog entry for invalid JSON errors', async () => {
Expand All @@ -252,6 +251,123 @@ describe('uv plugin', () => {

const catalog = err.errorCatalog;
expect(catalog).toBeInstanceOf(CLI.GeneralCLIFailureError);
expect(catalog!.detail).toBe('Unable to process dependency information');
expect(catalog?.detail).toBe('Unable to process dependency information');
});

it('throws a generic error when depgraph command fails with --json error', async () => {
const errorJSON = `{
"ok": false,
"error": "/path/to/uv version 0.9.9 is not supported. Minimum required version is 0.9.29",
"path": "/cwd/uv-project"
}`;
execGoCommandSpy.mockResolvedValueOnce(mockResult(errorJSON, 2, ''));

const err: CustomError = await inspect('.', 'uv.lock').catch((e) => e);

expect(err).toBeInstanceOf(CustomError);
expect(err.message).toBe(
'/path/to/uv version 0.9.9 is not supported. Minimum required version is 0.9.29',
);
expect(err.errorCatalog).toBeInstanceOf(CLI.GeneralCLIFailureError);
expect(err.errorCatalog?.detail).toBe(
'/path/to/uv version 0.9.9 is not supported. Minimum required version is 0.9.29',
);
});

it('passes --internal-uv-workspace-packages when allProjects is true', async () => {
const workspaceResult = JSON.stringify({
depGraph: MOCK_DEP_GRAPH_DATA,
targetFile: 'pyproject.toml',
});
execGoCommandSpy.mockResolvedValueOnce(mockResult(workspaceResult));

await inspect('.', 'uv.lock', { allProjects: true });

expect(execGoCommandSpy).toHaveBeenCalledWith(
[
'depgraph',
'--file=uv.lock',
'--use-sbom-resolution',
'--json',
'--internal-uv-workspace-packages',
],
{ cwd: '.' },
);
});

it('does not pass --internal-uv-workspace-packages when allProjects is false', async () => {
await inspect('.', 'uv.lock');

expect(execGoCommandSpy).toHaveBeenCalledWith(
['depgraph', '--file=uv.lock', '--use-sbom-resolution', '--json'],
{ cwd: '.' },
);
});

it('handles JSONL dep graphs from uv workspace when --all-projects is true', async () => {
const depGraphA: DepGraphData = {
schemaVersion: '1.3.0',
pkgManager: { name: 'uv' },
pkgs: [{ id: 'pkg-a@1.0.0', info: { name: 'pkg-a', version: '1.0.0' } }],
graph: {
rootNodeId: 'pkg-a@1.0.0',
nodes: [{ nodeId: 'pkg-a@1.0.0', pkgId: 'pkg-a@1.0.0', deps: [] }],
},
};
const depGraphB: DepGraphData = {
schemaVersion: '1.3.0',
pkgManager: { name: 'uv' },
pkgs: [{ id: 'pkg-b@2.0.0', info: { name: 'pkg-b', version: '2.0.0' } }],
graph: {
rootNodeId: 'pkg-b@2.0.0',
nodes: [{ nodeId: 'pkg-b@2.0.0', pkgId: 'pkg-b@2.0.0', deps: [] }],
},
};

const jsonl = [
JSON.stringify({ depGraph: depGraphA, targetFile: 'pyproject.toml' }),
JSON.stringify({
depGraph: depGraphB,
targetFile: 'packages/pkg-b/pyproject.toml',
}),
].join('\n');

execGoCommandSpy.mockResolvedValueOnce(mockResult(jsonl));

const result = await inspect('.', 'uv.lock', {
allProjects: true,
} as any);

expect(result.plugin).toEqual({
name: 'snyk-uv-plugin',
packageManager: 'uv',
targetFile: 'pyproject.toml',
});

expect(result.scannedProjects).toHaveLength(2);

const firstProject = result.scannedProjects[0];
expect(firstProject.targetFile).toBe('pyproject.toml');
expect(firstProject.depGraph?.rootPkg).toEqual({
name: 'pkg-a',
version: '1.0.0',
});

const secondProject = result.scannedProjects[1];
expect(secondProject.targetFile).toBe('packages/pkg-b/pyproject.toml');
expect(secondProject.depGraph?.rootPkg).toEqual({
name: 'pkg-b',
version: '2.0.0',
});
});

it('handles a single dep graph object (backward compatibility)', async () => {
const result = await inspect('.', 'uv.lock');

expect(result.scannedProjects).toHaveLength(1);
expect(result.scannedProjects[0].depGraph?.rootPkg).toEqual({
name: 'uv-project',
version: '0.1.0',
});
});
});
Loading