From 48e3995b725d991117fa4f9266084ae5c7d5653a Mon Sep 17 00:00:00 2001 From: Brian Gardiner Date: Wed, 3 Dec 2025 09:29:56 -0500 Subject: [PATCH 1/2] fix: use correct projectName in container monitor JSON output The projectName field in 'snyk container monitor --json' output was incorrectly set to monitorResult.id (the monitor's public ID) instead of monitorResult.projectName (the actual project name). This caused the JSON output to display a UUID instead of the project's display name (e.g., the image name or --project-name value). Changes: - Fixed src/lib/ecosystems/monitor.ts line 214 to use monitorResult.projectName - Added unit test with mocked registry response to validate the fix - Updated acceptance tests with correct expected projectName values --- src/lib/ecosystems/monitor.ts | 2 +- .../container-deb-scan-result.json | 27 +++++++++++ ...pendencies-response-with-project-name.json | 12 +++++ .../snyk-container/container.spec.ts | 19 +++++++- .../unit/ecosystems-monitor-docker.spec.ts | 48 +++++++++++++++++++ 5 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/container-projects/container-deb-scan-result.json create mode 100644 test/fixtures/container-projects/monitor-dependencies-response-with-project-name.json diff --git a/src/lib/ecosystems/monitor.ts b/src/lib/ecosystems/monitor.ts index 929bc088b7..30821f1169 100644 --- a/src/lib/ecosystems/monitor.ts +++ b/src/lib/ecosystems/monitor.ts @@ -218,7 +218,7 @@ export async function getFormattedMonitorOutput( ok: true, data: monOutput, path: monitorResult.path, - projectName: monitorResult.id, + projectName: monitorResult.projectName, }); } for (const monitorError of errors) { diff --git a/test/fixtures/container-projects/container-deb-scan-result.json b/test/fixtures/container-projects/container-deb-scan-result.json new file mode 100644 index 0000000000..25b4ffff93 --- /dev/null +++ b/test/fixtures/container-projects/container-deb-scan-result.json @@ -0,0 +1,27 @@ +{ + "identity": { + "type": "deb" + }, + "facts": [ + { + "type": "depGraph", + "data": { + "schemaVersion": "1.2.0", + "pkgManager": { + "name": "deb", + "repositories": [{"alias": "debian:11"}] + }, + "pkgs": [{"id": "alpine@3.18", "info": {"name": "alpine", "version": "3.18"}}], + "graph": { + "rootNodeId": "root-node", + "nodes": [{"nodeId": "root-node", "pkgId": "alpine@3.18", "deps": []}] + } + } + } + ], + "target": { + "image": "alpine:latest" + }, + "name": "my-custom-project-name" +} + diff --git a/test/fixtures/container-projects/monitor-dependencies-response-with-project-name.json b/test/fixtures/container-projects/monitor-dependencies-response-with-project-name.json new file mode 100644 index 0000000000..086f7c1783 --- /dev/null +++ b/test/fixtures/container-projects/monitor-dependencies-response-with-project-name.json @@ -0,0 +1,12 @@ +{ + "ok": true, + "org": "test-org", + "id": "7c7305e2-fbcb-44d7-8fbf-8367371c509f", + "isMonitored": true, + "licensesPolicy": null, + "uri": "https://app.snyk.io/org/test-org/project/3dda9b21-ca42-4de6-be7a-85696fa6e866/history/f60dce17-8a72-4cca-8a76-e9c88df546aa", + "trialStarted": false, + "path": "/srv", + "projectName": "my-custom-project-name" +} + diff --git a/test/jest/acceptance/snyk-container/container.spec.ts b/test/jest/acceptance/snyk-container/container.spec.ts index 478111e1cb..2e23878c0b 100644 --- a/test/jest/acceptance/snyk-container/container.spec.ts +++ b/test/jest/acceptance/snyk-container/container.spec.ts @@ -18,8 +18,8 @@ describe('snyk container', () => { }); } - const TEST_DISTROLESS_STATIC_IMAGE = - 'gcr.io/distroless/static@sha256:7198a357ff3a8ef750b041324873960cf2153c11cc50abb9d8d5f8bb089f6b4e'; + const TEST_DISTROLESS_STATIC_IMAGE_NAME = 'gcr.io/distroless/static'; + const TEST_DISTROLESS_STATIC_IMAGE = `${TEST_DISTROLESS_STATIC_IMAGE_NAME}@sha256:7198a357ff3a8ef750b041324873960cf2153c11cc50abb9d8d5f8bb089f6b4e`; const TEST_DISTROLESS_STATIC_IMAGE_DEPGRAPH = { schemaVersion: '1.3.0', pkgManager: { @@ -612,6 +612,7 @@ DepGraph end`, expect.objectContaining({ ok: true, packageManager: 'deb', + projectName: `docker-image|${TEST_DISTROLESS_STATIC_IMAGE_NAME}`, manageUrl: expect.stringContaining('://'), scanResult: expect.objectContaining({ facts: expect.arrayContaining([ @@ -646,6 +647,7 @@ DepGraph end`, expect.objectContaining({ ok: true, packageManager: 'deb', + projectName: 'docker-image|snyk/snyk', manageUrl: expect.stringContaining('://'), scanResult: expect.objectContaining({ facts: expect.arrayContaining([ @@ -668,6 +670,7 @@ DepGraph end`, expect.objectContaining({ ok: true, packageManager: 'gomodules', + projectName: 'docker-image|snyk/snyk:/usr/local/bin/snyk', manageUrl: expect.stringContaining('://'), scanResult: expect.objectContaining({ facts: expect.arrayContaining([ @@ -685,6 +688,18 @@ DepGraph end`, ]), ); }); + + it('snyk container monitor json returns custom projectName when --project-name is provided', async () => { + const customProjectName = 'my-custom-project-name'; + const { code, stdout } = await runSnykCLI( + `container monitor --platform=linux/amd64 --project-name=${customProjectName} --json ${TEST_DISTROLESS_STATIC_IMAGE}`, + ); + expect(code).toEqual(0); + const result = JSON.parse(stdout); + + // projectName should match the --project-name flag value + expect(result.projectName).toBe(customProjectName); + }); }); describe('snyk container monitor supports --target-reference', () => { diff --git a/test/jest/unit/ecosystems-monitor-docker.spec.ts b/test/jest/unit/ecosystems-monitor-docker.spec.ts index 1fbcb57776..757873ca5e 100644 --- a/test/jest/unit/ecosystems-monitor-docker.spec.ts +++ b/test/jest/unit/ecosystems-monitor-docker.spec.ts @@ -186,4 +186,52 @@ describe('monitorEcosystem docker/container', () => { makeRequestSpy.mock.calls[0][0].body.pruneRepeatedSubdependencies, ).toBeUndefined(); }); + + it('should return projectName from registry response in JSON output', async () => { + const containerScanResult = readJsonFixture( + 'container-deb-scan-result.json', + ) as ScanResult; + const monitorDependenciesResponse = readJsonFixture( + 'monitor-dependencies-response-with-project-name.json', + ) as ecosystemsTypes.MonitorDependenciesResponse; + + jest + .spyOn(dockerPlugin, 'scan') + .mockResolvedValue({ scanResults: [containerScanResult] }); + jest + .spyOn(request, 'makeRequest') + .mockResolvedValue(monitorDependenciesResponse); + + const results: Array = []; + + const [monitorResults, monitorErrors] = await ecosystems.monitorEcosystem( + 'docker', + ['/srv'], + { + path: '/srv', + docker: true, + org: 'test-org', + }, + ); + + const jsonOutput = await getFormattedMonitorOutput( + results, + monitorResults, + monitorErrors, + { + path: '/srv', + docker: true, + org: 'test-org', + json: true, + } as Options, + ); + + const parsedOutput = JSON.parse(jsonOutput); + + // projectName should be the actual project name from the registry, not the id (UUID) + expect(parsedOutput.projectName).toBe('my-custom-project-name'); + expect(parsedOutput.projectName).not.toBe( + '7c7305e2-fbcb-44d7-8fbf-8367371c509f', + ); + }); }); From 1e0425e163f454ca6bbcf388d88c48e9061381b9 Mon Sep 17 00:00:00 2001 From: Brian Gardiner Date: Thu, 5 Feb 2026 09:28:52 -0500 Subject: [PATCH 2/2] feat: add disableContainerMonitorProjectNameFix feature flag Add escape hatch feature flag to allow reverting to legacy behavior (using monitor id instead of projectName) if issues arise during rollout. By default, the correct projectName is used. --- src/cli/commands/constants.ts | 2 + src/cli/commands/monitor/index.ts | 12 +++ src/lib/ecosystems/monitor.ts | 5 +- src/lib/types.ts | 1 + .../cli-commands/test-and-monitor.spec.ts | 95 +++++++++++++++++++ .../unit/ecosystems-monitor-docker.spec.ts | 55 ++++++++++- 6 files changed, 167 insertions(+), 3 deletions(-) diff --git a/src/cli/commands/constants.ts b/src/cli/commands/constants.ts index f7074f16a6..30802f846a 100644 --- a/src/cli/commands/constants.ts +++ b/src/cli/commands/constants.ts @@ -2,6 +2,8 @@ export const SCAN_USR_LIB_JARS_FEATURE_FLAG = 'scanUsrLibJars'; export const CONTAINER_CLI_APP_VULNS_ENABLED_FEATURE_FLAG = 'containerCliAppVulnsEnabled'; +export const DISABLE_CONTAINER_MONITOR_PROJECT_NAME_FIX_FEATURE_FLAG = + 'disableContainerMonitorProjectNameFix'; // CLI option names export const INCLUDE_SYSTEM_JARS_OPTION = 'include-system-jars'; diff --git a/src/cli/commands/monitor/index.ts b/src/cli/commands/monitor/index.ts index 0b8c63be81..9c073327fe 100644 --- a/src/cli/commands/monitor/index.ts +++ b/src/cli/commands/monitor/index.ts @@ -55,6 +55,7 @@ import { import { SCAN_USR_LIB_JARS_FEATURE_FLAG, CONTAINER_CLI_APP_VULNS_ENABLED_FEATURE_FLAG, + DISABLE_CONTAINER_MONITOR_PROJECT_NAME_FIX_FEATURE_FLAG, INCLUDE_SYSTEM_JARS_OPTION, EXCLUDE_APP_VULNS_OPTION, APP_VULNS_OPTION, @@ -147,6 +148,17 @@ export default async function monitor(...args0: MethodArgs): Promise { if (scanUsrLibJarsEnabled) { options[INCLUDE_SYSTEM_JARS_OPTION] = true; } + + // Check disableContainerMonitorProjectNameFix feature flag + // When enabled, reverts to legacy behavior (using id instead of projectName in JSON output) + const disableContainerMonitorProjectNameFix = await hasFeatureFlagOrDefault( + DISABLE_CONTAINER_MONITOR_PROJECT_NAME_FIX_FEATURE_FLAG, + options, + false, + ); + if (disableContainerMonitorProjectNameFix) { + options.disableContainerMonitorProjectNameFix = true; + } } // Handles no image arg provided to the container command until diff --git a/src/lib/ecosystems/monitor.ts b/src/lib/ecosystems/monitor.ts index 30821f1169..7ed79a21b5 100644 --- a/src/lib/ecosystems/monitor.ts +++ b/src/lib/ecosystems/monitor.ts @@ -218,7 +218,10 @@ export async function getFormattedMonitorOutput( ok: true, data: monOutput, path: monitorResult.path, - projectName: monitorResult.projectName, + // Use correct projectName by default; feature flag reverts to legacy behavior (id) + projectName: options.disableContainerMonitorProjectNameFix + ? monitorResult.id + : monitorResult.projectName, }); } for (const monitorError of errors) { diff --git a/src/lib/types.ts b/src/lib/types.ts index a00961f412..a1146d68a0 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -130,6 +130,7 @@ export interface Options { // Feature Flags useImprovedDotnetWithoutPublish?: boolean; + disableContainerMonitorProjectNameFix?: boolean; } // TODO(kyegupov): catch accessing ['undefined-properties'] via noImplicitAny diff --git a/test/jest/unit/cli-commands/test-and-monitor.spec.ts b/test/jest/unit/cli-commands/test-and-monitor.spec.ts index 5ecd1b77ba..9bce7aa380 100644 --- a/test/jest/unit/cli-commands/test-and-monitor.spec.ts +++ b/test/jest/unit/cli-commands/test-and-monitor.spec.ts @@ -3,6 +3,7 @@ import { DOTNET_WITHOUT_PUBLISH_FEATURE_FLAG } from '../../../../src/lib/package import * as featureFlags from '../../../../src/lib/feature-flags'; import { SCAN_USR_LIB_JARS_FEATURE_FLAG, + DISABLE_CONTAINER_MONITOR_PROJECT_NAME_FIX_FEATURE_FLAG, INCLUDE_SYSTEM_JARS_OPTION, EXCLUDE_APP_VULNS_OPTION, } from '../../../../src/cli/commands/constants'; @@ -403,4 +404,98 @@ describe('monitor & test', () => { }); }); }); + + describe('docker disableContainerMonitorProjectNameFix feature flag', () => { + beforeEach(() => { + getEcosystemSpy.mockReturnValue(undefined); + analyticsSpy.mockReturnValue(false); + }); + + describe('monitor command', () => { + it('should set disableContainerMonitorProjectNameFix on options when feature flag is enabled (to revert to legacy behavior)', async () => { + const options: any = { + docker: true, + [EXCLUDE_APP_VULNS_OPTION]: true, + }; + + (featureFlags.hasFeatureFlagOrDefault as jest.Mock).mockImplementation( + (flag: string) => { + if (flag === DISABLE_CONTAINER_MONITOR_PROJECT_NAME_FIX_FEATURE_FLAG) { + return Promise.resolve(true); + } + return Promise.resolve(false); + }, + ); + + try { + await monitor('docker-image:latest', options); + } catch (error) { + // We expect this to fail since we are not mocking all dependencies. + // We only care about the feature flag being called correctly. + } + + expect(featureFlags.hasFeatureFlagOrDefault).toHaveBeenCalledWith( + DISABLE_CONTAINER_MONITOR_PROJECT_NAME_FIX_FEATURE_FLAG, + expect.objectContaining({ + docker: true, + }), + false, + ); + + // When flag is enabled, it triggers legacy behavior (using id instead of projectName) + expect(options.disableContainerMonitorProjectNameFix).toBe(true); + }); + + it('should not set disableContainerMonitorProjectNameFix on options by default (uses new correct behavior)', async () => { + const options: any = { + docker: true, + [EXCLUDE_APP_VULNS_OPTION]: true, + }; + + (featureFlags.hasFeatureFlagOrDefault as jest.Mock).mockImplementation( + () => { + return Promise.resolve(false); + }, + ); + + try { + await monitor('docker-image:latest', options); + } catch (error) { + // We expect this to fail since we are not mocking all dependencies. + // We only care about the feature flag being called correctly. + } + + expect(featureFlags.hasFeatureFlagOrDefault).toHaveBeenCalledWith( + DISABLE_CONTAINER_MONITOR_PROJECT_NAME_FIX_FEATURE_FLAG, + expect.objectContaining({ + docker: true, + }), + false, + ); + + // When flag is not set, uses new correct behavior (projectName) + expect(options.disableContainerMonitorProjectNameFix).toBeUndefined(); + }); + + it('should not check disableContainerMonitorProjectNameFix feature flag for non-docker scans', async () => { + const options: any = { + docker: false, + }; + + try { + await monitor('path/to/project', options); + } catch (error) { + // We expect this to fail since we are not mocking all dependencies. + // We only care about the options being set correctly. + } + + expect(featureFlags.hasFeatureFlagOrDefault).not.toHaveBeenCalledWith( + DISABLE_CONTAINER_MONITOR_PROJECT_NAME_FIX_FEATURE_FLAG, + options, + false, + ); + expect(options.disableContainerMonitorProjectNameFix).toBeUndefined(); + }); + }); + }); }); diff --git a/test/jest/unit/ecosystems-monitor-docker.spec.ts b/test/jest/unit/ecosystems-monitor-docker.spec.ts index 757873ca5e..b2f113b9cc 100644 --- a/test/jest/unit/ecosystems-monitor-docker.spec.ts +++ b/test/jest/unit/ecosystems-monitor-docker.spec.ts @@ -187,7 +187,7 @@ describe('monitorEcosystem docker/container', () => { ).toBeUndefined(); }); - it('should return projectName from registry response in JSON output', async () => { + it('should return projectName from registry response in JSON output by default', async () => { const containerScanResult = readJsonFixture( 'container-deb-scan-result.json', ) as ScanResult; @@ -223,15 +223,66 @@ describe('monitorEcosystem docker/container', () => { docker: true, org: 'test-org', json: true, + // Feature flag not set - uses new correct behavior by default } as Options, ); const parsedOutput = JSON.parse(jsonOutput); - // projectName should be the actual project name from the registry, not the id (UUID) + // projectName should be the actual project name from the registry by default expect(parsedOutput.projectName).toBe('my-custom-project-name'); expect(parsedOutput.projectName).not.toBe( '7c7305e2-fbcb-44d7-8fbf-8367371c509f', ); }); + + it('should return id as projectName in JSON output when feature flag is enabled (legacy behavior)', async () => { + const containerScanResult = readJsonFixture( + 'container-deb-scan-result.json', + ) as ScanResult; + const monitorDependenciesResponse = readJsonFixture( + 'monitor-dependencies-response-with-project-name.json', + ) as ecosystemsTypes.MonitorDependenciesResponse; + + jest + .spyOn(dockerPlugin, 'scan') + .mockResolvedValue({ scanResults: [containerScanResult] }); + jest + .spyOn(request, 'makeRequest') + .mockResolvedValue(monitorDependenciesResponse); + + const results: Array = []; + + const [monitorResults, monitorErrors] = await ecosystems.monitorEcosystem( + 'docker', + ['/srv'], + { + path: '/srv', + docker: true, + org: 'test-org', + }, + ); + + const jsonOutput = await getFormattedMonitorOutput( + results, + monitorResults, + monitorErrors, + { + path: '/srv', + docker: true, + org: 'test-org', + json: true, + // Feature flag enabled - reverts to legacy behavior + disableContainerMonitorProjectNameFix: true, + } as Options, + ); + + const parsedOutput = JSON.parse(jsonOutput); + + // projectName should be the id (UUID) when feature flag is enabled (legacy escape hatch) + expect(parsedOutput.projectName).toBe( + '7c7305e2-fbcb-44d7-8fbf-8367371c509f', + ); + expect(parsedOutput.projectName).not.toBe('my-custom-project-name'); + }); });