Skip to content

Commit

Permalink
chore: implement --print-graph for container (#4646)
Browse files Browse the repository at this point in the history
This commit adds support for the `--print-graph` argument that will
print the dependency graph after a container scan.
We need this feature in order to build the `snyk container sbom` command
later on.

The flag works the exact same way as the normal `snyk test --print-graph` does.

See [LUM-283](https://snyksec.atlassian.net/browse/LUM-283).
  • Loading branch information
tommyknows authored Aug 15, 2023
1 parent c8c61a0 commit dd56350
Show file tree
Hide file tree
Showing 5 changed files with 167 additions and 16 deletions.
16 changes: 7 additions & 9 deletions src/lib/ecosystems/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ import { spinner } from '../../lib/spinner';
import { Ecosystem, ScanResult, TestResult } from './types';
import { getPlugin } from './plugins';
import { TestDependenciesResponse } from '../snyk-test/legacy';
import { assembleQueryString } from '../snyk-test/common';
import {
assembleQueryString,
depGraphToOutputString,
} from '../snyk-test/common';
import { getAuthHeader } from '../api-token';
import { resolveAndTestFacts } from './resolve-test-facts';
import { isUnmanagedEcosystem } from './common';
import { convertDepGraph, getUnmanagedDepGraph } from './unmanaged/utils';
import { jsonStringifyLargeObject } from '../json';

type ScanResultsByPath = { [dir: string]: ScanResult[] };

Expand Down Expand Up @@ -92,13 +94,9 @@ export async function formatUnmanagedResults(
const [result] = await getUnmanagedDepGraph(results);
const depGraph = convertDepGraph(result);

const template = `DepGraph data:
${jsonStringifyLargeObject(depGraph)}
DepGraph target:
${target}
DepGraph end`;

return TestCommandResult.createJsonTestCommandResult(template);
return TestCommandResult.createJsonTestCommandResult(
depGraphToOutputString(depGraph, target),
);
}

async function testDependencies(
Expand Down
50 changes: 48 additions & 2 deletions src/lib/snyk-test/assemble-payloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import * as path from 'path';
import config from '../config';
import { isCI } from '../is-ci';
import { getPlugin } from '../ecosystems';
import { Ecosystem } from '../ecosystems/types';
import { Ecosystem, ContainerTarget, ScanResult } from '../ecosystems/types';
import { Options, PolicyOptions, TestOptions } from '../types';
import { Payload } from './types';
import { assembleQueryString } from './common';
import { assembleQueryString, depGraphToOutputString } from './common';
import { spinner } from '../spinner';
import { findAndLoadPolicyForScanResult } from '../ecosystems/policy';
import { getAuthHeader } from '../../lib/api-token';
import { DockerImageNotFoundError } from '../errors';
import { DepGraph } from '@snyk/dep-graph';

export async function assembleEcosystemPayloads(
ecosystem: Ecosystem,
Expand Down Expand Up @@ -52,6 +53,21 @@ export async function assembleEcosystemPayloads(
scanResult.name =
options['project-name'] || config.PROJECT_NAME || scanResult.name;

if (options['print-graph'] && !options['print-deps']) {
// not every scanResult has a 'depGraph' fact, for example the JAR
// fingerprints. I don't think we have another option than to skip
// those.
const dg = scanResult.facts.find((dg) => dg.type === 'depGraph');
if (dg) {
console.log(
depGraphToOutputString(
dg.data.toJSON(),
constructProjectName(scanResult),
),
);
}
}

payloads.push({
method: 'POST',
url: `${config.API}${options.testDepGraphDockerEndpoint ||
Expand Down Expand Up @@ -82,3 +98,33 @@ export async function assembleEcosystemPayloads(
spinner.clear<void>(spinnerLbl)();
}
}

// constructProjectName attempts to construct the project name the same way that
// registry does. This is a bit difficult because in Registry, the code is
// distributed over multiple functions and files that need to be kept in sync...
function constructProjectName(sr: ScanResult): string {
let suffix = '';
if (sr.identity.targetFile) {
suffix = ':' + sr.identity.targetFile;
}

if (sr.name) {
return sr.name + suffix;
}

const targetImage = (sr.target as ContainerTarget | undefined)?.image;
if (targetImage) {
return targetImage + suffix;
}

const dgFact = sr.facts.find((d) => d.type === 'depGraph');
// not every scanResult has a depGraph, for example the JAR fingerprints.
if (dgFact) {
const name = (dgFact.data as DepGraph | undefined)?.rootPkg.name;
if (name) {
return name + suffix;
}
}

return 'no-name' + suffix;
}
15 changes: 15 additions & 0 deletions src/lib/snyk-test/common.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import config from '../config';
import { color } from '../theme';
import { DepGraphData } from '@snyk/dep-graph';
import { jsonStringifyLargeObject } from '../json';

export function assembleQueryString(options) {
const org = options.org || config.org || null;
Expand Down Expand Up @@ -68,3 +70,16 @@ export type FailOn = 'all' | 'upgradable' | 'patchable';

export const RETRY_ATTEMPTS = 3;
export const RETRY_DELAY = 500;

// depGraphData formats the given depGrahData with the targetName as expected by
// the `depgraph` CLI workflow.
export function depGraphToOutputString(
dg: DepGraphData,
targetName: string,
): string {
return `DepGraph data:
${jsonStringifyLargeObject(dg)}
DepGraph target:
${targetName}
DepGraph end`;
}
9 changes: 4 additions & 5 deletions src/lib/snyk-test/run-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import { icon } from '../theme';
import { parsePackageString as moduleToObject } from 'snyk-module';
import * as depGraphLib from '@snyk/dep-graph';
import * as theme from '../../lib/theme';
import { jsonStringifyLargeObject } from '../../lib/json';
import * as pMap from 'p-map';

import {
Expand Down Expand Up @@ -747,7 +746,7 @@ async function assembleLocalPayloads(

// print dep graph if `--print-graph` is set
if (options['print-graph'] && !options['print-deps']) {
await spinner.clear<void>(spinnerLbl)();
spinner.clear<void>(spinnerLbl)();
let root: depGraphLib.DepGraph;
if (scannedProject.depGraph) {
root = pkg as depGraphLib.DepGraph;
Expand All @@ -759,9 +758,9 @@ async function assembleLocalPayloads(
);
}

console.log('DepGraph data:');
console.log(jsonStringifyLargeObject(root.toJSON()));
console.log('DepGraph target:\n' + targetFile + '\nDepGraph end');
console.log(
common.depGraphToOutputString(root.toJSON(), targetFile || ''),
);
}

const body: PayloadBody = {
Expand Down
93 changes: 93 additions & 0 deletions test/jest/acceptance/snyk-container/container.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as os from 'os';
import { startSnykCLI, TestCLI } from '../../util/startSnykCLI';
import { runSnykCLI } from '../../util/runSnykCLI';

jest.setTimeout(1000 * 60);

Expand Down Expand Up @@ -37,4 +38,96 @@ describe('snyk container', () => {
timeout: 60 * 1000,
});
});
it('prints dep graph with --print-graph flag', async () => {
const { code, stdout } = await runSnykCLI(
'container test --print-graph gcr.io/distroless/static@sha256:7198a357ff3a8ef750b041324873960cf2153c11cc50abb9d8d5f8bb089f6b4e',
);

expect(code).toBe(0);
expect(stdout).toContain('DepGraph data:');
expect(stdout).toContain(
`DepGraph target:
docker-image|gcr.io/distroless/static
DepGraph end`,
);
const jsonDGStr = stdout
.split('DepGraph data:')[1]
.split('DepGraph target:')[0];
const jsonDG = JSON.parse(jsonDGStr);
expect(jsonDG).toMatchObject({
schemaVersion: '1.3.0',
pkgManager: {
name: 'deb',
repositories: [
{
alias: 'debian:11',
},
],
},
pkgs: [
{
id: 'docker-image|gcr.io/distroless/static@',
info: {
name: 'docker-image|gcr.io/distroless/static',
},
},
{
id: 'base-files@11.1+deb11u7',
info: {
name: 'base-files',
version: '11.1+deb11u7',
},
},
{
id: 'netbase@6.3',
info: {
name: 'netbase',
version: '6.3',
},
},
{
id: 'tzdata@2021a-1+deb11u10',
info: {
name: 'tzdata',
version: '2021a-1+deb11u10',
},
},
],
graph: {
rootNodeId: 'root-node',
nodes: [
{
nodeId: 'root-node',
pkgId: 'docker-image|gcr.io/distroless/static@',
deps: [
{
nodeId: 'base-files@11.1+deb11u7',
},
{
nodeId: 'netbase@6.3',
},
{
nodeId: 'tzdata@2021a-1+deb11u10',
},
],
},
{
nodeId: 'base-files@11.1+deb11u7',
pkgId: 'base-files@11.1+deb11u7',
deps: [],
},
{
nodeId: 'netbase@6.3',
pkgId: 'netbase@6.3',
deps: [],
},
{
nodeId: 'tzdata@2021a-1+deb11u10',
pkgId: 'tzdata@2021a-1+deb11u10',
deps: [],
},
],
},
});
});
});

0 comments on commit dd56350

Please sign in to comment.