Skip to content

Commit 2f19d4f

Browse files
committed
feat: add a CLI option to print effective dep graphs
1 parent d6473a9 commit 2f19d4f

File tree

4 files changed

+263
-1
lines changed

4 files changed

+263
-1
lines changed

src/lib/snyk-test/common.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import config from '../config';
66
import { color } from '../theme';
77
import { Options } from '../types';
88
import { ConcatStream } from '../stream';
9+
import { ContainerTarget, GitTarget } from '../project-metadata/types';
910

1011
export function assembleQueryString(options) {
1112
const org = options.org || config.org || null;
@@ -99,3 +100,37 @@ export async function printDepGraph(
99100
export function shouldPrintDepGraph(opts: Options): boolean {
100101
return opts['print-graph'] && !opts['print-deps'];
101102
}
103+
104+
/**
105+
* printEffectiveDepGraph writes the given, possibly pruned dep-graph and target file to the destination
106+
* stream as a JSON object containing both depGraph, normalisedTargetFile and targetFile from plugin.
107+
* This allows extracting the effective dep-graph which is being used for the test.
108+
*/
109+
export async function printEffectiveDepGraph(
110+
depGraph: DepGraphData,
111+
normalisedTargetFile: string,
112+
targetFileFromPlugin: string | undefined,
113+
target: GitTarget | ContainerTarget | null | undefined,
114+
destination: Writable,
115+
): Promise<void> {
116+
return new Promise((res, rej) => {
117+
const effectiveGraphOutput = {
118+
depGraph,
119+
normalisedTargetFile,
120+
targetFileFromPlugin,
121+
target,
122+
};
123+
124+
new ConcatStream(
125+
new JsonStreamStringify(effectiveGraphOutput),
126+
Readable.from('\n'),
127+
)
128+
.on('end', res)
129+
.on('error', rej)
130+
.pipe(destination);
131+
});
132+
}
133+
134+
export function shouldPrintEffectiveDepGraph(opts: Options): boolean {
135+
return !!opts['print-effective-graph'];
136+
}

src/lib/snyk-test/run-test.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,10 @@ import {
4040
RETRY_ATTEMPTS,
4141
RETRY_DELAY,
4242
printDepGraph,
43+
printEffectiveDepGraph,
4344
assembleQueryString,
4445
shouldPrintDepGraph,
46+
shouldPrintEffectiveDepGraph,
4547
} from './common';
4648
import config from '../config';
4749
import * as analytics from '../analytics';
@@ -372,7 +374,10 @@ export async function runTest(
372374
// At this point managed ecosystems have dependency graphs printed.
373375
// Containers however require another roundtrip to get all the
374376
// dependency graph artifacts for printing.
375-
if (!options.docker && shouldPrintDepGraph(options)) {
377+
if (
378+
!options.docker &&
379+
(shouldPrintDepGraph(options) || shouldPrintEffectiveDepGraph(options))
380+
) {
376381
const results: TestResult[] = [];
377382
return results;
378383
}
@@ -853,6 +858,18 @@ async function assembleLocalPayloads(
853858
if (packageManager) {
854859
depGraph = await pruneGraph(depGraph, packageManager, pruneIsRequired);
855860
}
861+
862+
if (shouldPrintEffectiveDepGraph(options)) {
863+
spinner.clear<void>(spinnerLbl)();
864+
await printEffectiveDepGraph(
865+
depGraph.toJSON(),
866+
targetFile,
867+
project.plugin.targetFile,
868+
target,
869+
process.stdout,
870+
);
871+
}
872+
856873
body.depGraph = depGraph;
857874

858875
const reqUrl =

src/lib/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export interface Options {
6767
'print-deps'?: boolean;
6868
'print-tree'?: boolean;
6969
'print-dep-paths'?: boolean;
70+
'print-effective-graph'?: boolean;
7071
'remote-repo-url'?: string;
7172
criticality?: string;
7273
scanAllUnmanaged?: boolean;
Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import { fakeServer } from '../../acceptance/fake-server';
2+
import { createProjectFromFixture } from '../util/createProject';
3+
import { runSnykCLI } from '../util/runSnykCLI';
4+
import { getServerPort } from '../util/getServerPort';
5+
import * as path from 'path';
6+
7+
jest.setTimeout(1000 * 30);
8+
9+
describe('`test` command with `--print-effective-graph` option', () => {
10+
let server;
11+
let env: Record<string, string>;
12+
13+
beforeAll((done) => {
14+
const port = getServerPort(process);
15+
const baseApi = '/api/v1';
16+
env = {
17+
...process.env,
18+
SNYK_API: 'http://localhost:' + port + baseApi,
19+
SNYK_HOST: 'http://localhost:' + port,
20+
SNYK_TOKEN: '123456789',
21+
SNYK_INTEGRATION_NAME: 'JENKINS',
22+
SNYK_INTEGRATION_VERSION: '1.2.3',
23+
};
24+
server = fakeServer(baseApi, env.SNYK_TOKEN);
25+
server.listen(port, () => {
26+
done();
27+
});
28+
});
29+
30+
afterEach(() => {
31+
server.restore();
32+
});
33+
34+
afterAll((done) => {
35+
server.close(() => {
36+
done();
37+
});
38+
});
39+
40+
// test cases for `--print-effective-graph` option:
41+
// effective graph for project with no deps
42+
// effective graph for project with deps
43+
// effective graph for project with deps and --all-projects
44+
45+
it('works for project with no deps', async () => {
46+
const project = await createProjectFromFixture('print-graph-no-deps');
47+
const { code, stdout } = await runSnykCLI('test --print-effective-graph', {
48+
cwd: project.path(),
49+
env,
50+
});
51+
52+
expect(code).toEqual(0);
53+
54+
const jsonOutput = JSON.parse(stdout);
55+
56+
expect(jsonOutput.normalisedTargetFile).toBe('package.json');
57+
expect(jsonOutput.depGraph).toMatchObject({
58+
pkgManager: {
59+
name: 'npm',
60+
},
61+
pkgs: [
62+
{
63+
64+
info: {
65+
name: 'print-graph-no-deps',
66+
version: '1.0.0',
67+
},
68+
},
69+
],
70+
graph: {
71+
rootNodeId: 'root-node',
72+
nodes: [
73+
{
74+
nodeId: 'root-node',
75+
76+
deps: [],
77+
},
78+
],
79+
},
80+
});
81+
});
82+
83+
it('works for project with dep', async () => {
84+
const project = await createProjectFromFixture(
85+
'npm/with-vulnerable-lodash-dep',
86+
);
87+
server.setCustomResponse(
88+
await project.readJSON('test-dep-graph-result.json'),
89+
);
90+
const { code, stdout } = await runSnykCLI('test --print-effective-graph', {
91+
cwd: project.path(),
92+
env,
93+
});
94+
95+
expect(code).toEqual(0);
96+
97+
const jsonOutput = JSON.parse(stdout);
98+
99+
expect(jsonOutput.normalisedTargetFile).toBe('package-lock.json');
100+
101+
expect(jsonOutput.depGraph).toMatchObject({
102+
pkgManager: {
103+
name: 'npm',
104+
},
105+
pkgs: [
106+
{
107+
108+
info: {
109+
name: 'with-vulnerable-lodash-dep',
110+
version: '1.2.3',
111+
},
112+
},
113+
{
114+
115+
info: {
116+
name: 'lodash',
117+
version: '4.17.15',
118+
},
119+
},
120+
],
121+
graph: {
122+
rootNodeId: 'root-node',
123+
nodes: [
124+
{
125+
nodeId: 'root-node',
126+
127+
deps: [
128+
{
129+
nodeId: '[email protected]',
130+
},
131+
],
132+
},
133+
{
134+
nodeId: '[email protected]',
135+
136+
deps: [],
137+
info: {
138+
labels: {
139+
scope: 'prod',
140+
},
141+
},
142+
},
143+
],
144+
},
145+
});
146+
});
147+
148+
it('works with `--all-projects`', async () => {
149+
const project = await createProjectFromFixture(
150+
'print-graph-multiple-projects',
151+
);
152+
const { code, stdout } = await runSnykCLI(
153+
'test --all-projects --print-effective-graph',
154+
{
155+
cwd: project.path(),
156+
env,
157+
},
158+
);
159+
160+
expect(code).toEqual(0);
161+
162+
// With --all-projects, we get JSONL format (each JSON object on its own line)
163+
const lines = stdout
164+
.trim()
165+
.split('\n')
166+
.filter((line) => line.trim());
167+
expect(lines.length).toBeGreaterThan(0);
168+
169+
const jsonOutputFirstProject = JSON.parse(lines[0]);
170+
171+
expect(jsonOutputFirstProject).toHaveProperty('depGraph');
172+
expect(jsonOutputFirstProject).toHaveProperty('normalisedTargetFile');
173+
174+
// The first project should be processed
175+
expect(jsonOutputFirstProject.depGraph).toMatchObject({
176+
pkgManager: {
177+
name: 'npm',
178+
},
179+
pkgs: expect.arrayContaining([
180+
expect.objectContaining({
181+
info: expect.objectContaining({
182+
name: expect.stringMatching(/proj[12]/),
183+
}),
184+
}),
185+
]),
186+
graph: {
187+
rootNodeId: 'root-node',
188+
nodes: expect.any(Array),
189+
},
190+
});
191+
expect(jsonOutputFirstProject.normalisedTargetFile).toBe(
192+
path.join('proj1', 'package.json'),
193+
);
194+
195+
const jsonOutputSecondProject = JSON.parse(lines[1]);
196+
197+
expect(jsonOutputSecondProject).toHaveProperty('depGraph');
198+
expect(jsonOutputSecondProject).toHaveProperty('normalisedTargetFile');
199+
200+
expect(jsonOutputSecondProject.depGraph).toMatchObject({
201+
pkgManager: {
202+
name: 'npm',
203+
},
204+
});
205+
expect(jsonOutputSecondProject.normalisedTargetFile).toBe(
206+
path.join('proj2', 'package.json'),
207+
);
208+
});
209+
});

0 commit comments

Comments
 (0)