Skip to content
Open
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: 2 additions & 0 deletions src/cli/commands/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
12 changes: 12 additions & 0 deletions src/cli/commands/monitor/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -147,6 +148,17 @@ export default async function monitor(...args0: MethodArgs): Promise<any> {
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
Expand Down
5 changes: 4 additions & 1 deletion src/lib/ecosystems/monitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,10 @@ export async function getFormattedMonitorOutput(
ok: true,
data: monOutput,
path: monitorResult.path,
projectName: monitorResult.id,
// Use correct projectName by default; feature flag reverts to legacy behavior (id)
projectName: options.disableContainerMonitorProjectNameFix
? monitorResult.id
: monitorResult.projectName,
});
}
for (const monitorError of errors) {
Expand Down
1 change: 1 addition & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ export interface Options {

// Feature Flags
useImprovedDotnetWithoutPublish?: boolean;
disableContainerMonitorProjectNameFix?: boolean;
}

// TODO(kyegupov): catch accessing ['undefined-properties'] via noImplicitAny
Expand Down
27 changes: 27 additions & 0 deletions test/fixtures/container-projects/container-deb-scan-result.json
Original file line number Diff line number Diff line change
@@ -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"
}

Original file line number Diff line number Diff line change
@@ -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"
}

19 changes: 17 additions & 2 deletions test/jest/acceptance/snyk-container/container.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
Expand Down Expand Up @@ -675,6 +675,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([
Expand Down Expand Up @@ -709,6 +710,7 @@ DepGraph end`,
expect.objectContaining({
ok: true,
packageManager: 'deb',
projectName: 'docker-image|snyk/snyk',
manageUrl: expect.stringContaining('://'),
scanResult: expect.objectContaining({
facts: expect.arrayContaining([
Expand All @@ -731,6 +733,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([
Expand All @@ -748,6 +751,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', () => {
Expand Down
95 changes: 95 additions & 0 deletions test/jest/unit/cli-commands/test-and-monitor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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();
});
});
});
});
99 changes: 99 additions & 0 deletions test/jest/unit/ecosystems-monitor-docker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,4 +186,103 @@ describe('monitorEcosystem docker/container', () => {
makeRequestSpy.mock.calls[0][0].body.pruneRepeatedSubdependencies,
).toBeUndefined();
});

it('should return projectName from registry response in JSON output by default', 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<GoodResult | BadResult> = [];

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 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 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<GoodResult | BadResult> = [];

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');
});
});