Skip to content

Commit a8cd9b5

Browse files
committed
fix: upgrade docker-plugin to handle breaking change
This is a major version bump but the new plugin response is shimmed back to its old format to preserve backwards compatibility. This is done in preparation for switching to using the new plugin response completely.
1 parent c5ba9ef commit a8cd9b5

File tree

9 files changed

+764
-89
lines changed

9 files changed

+764
-89
lines changed

package-lock.json

Lines changed: 430 additions & 31 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
"private": true,
2929
"dependencies": {
3030
"@kubernetes/client-node": "^0.12.2",
31+
"@snyk/dep-graph": "1.21.0",
3132
"@types/async": "^3.2.3",
3233
"@types/child-process-promise": "^2.2.1",
3334
"@types/lru-cache": "^5.1.0",
@@ -43,7 +44,7 @@
4344
"needle": "^2.5.0",
4445
"sleep-promise": "^8.0.1",
4546
"snyk-config": "4.0.0",
46-
"snyk-docker-plugin": "2.8.0",
47+
"snyk-docker-plugin": "4.12.0",
4748
"source-map-support": "^0.5.16",
4849
"tunnel": "0.0.6",
4950
"typescript": "^3.8.3",
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import * as depGraphLib from '@snyk/dep-graph';
2+
import { ManifestFile, PluginResponse, ScanResult } from 'snyk-docker-plugin';
3+
4+
export interface PluginMetadata {
5+
name: 'snyk-docker-plugin';
6+
runtime: string | undefined;
7+
packageManager: string;
8+
dockerImageId: string;
9+
imageLayers: string[];
10+
}
11+
12+
/** @deprecated */
13+
export interface LegacyPluginResponse {
14+
plugin: PluginMetadata;
15+
package: DependencyTree;
16+
manifestFiles: ManifestFile[];
17+
hashes: string[];
18+
19+
/**
20+
* WARNING! This field was added by kubernetes-monitor.
21+
* It is not part of the normal plugin response. */
22+
imageMetadata: {
23+
image: string;
24+
imageTag: string;
25+
imageDigest?: string;
26+
};
27+
}
28+
29+
interface ExtractedFacts {
30+
depGraph: depGraphLib.DepGraph;
31+
manifestFiles?: ManifestFile[];
32+
hashes?: string[];
33+
imageLayers?: string[];
34+
rootFs?: string[];
35+
imageId?: string;
36+
imageOsReleasePrettyName?: string;
37+
platform?: string;
38+
}
39+
40+
/** @deprecated */
41+
export interface DependencyTree {
42+
name: string;
43+
type: string;
44+
targetOS?: {
45+
name: string;
46+
prettyName: string;
47+
version: string;
48+
};
49+
targetFile?: string;
50+
testedFiles?: string[];
51+
dependencies: any;
52+
version: string | undefined;
53+
dockerImageId?: string;
54+
docker?: {
55+
dockerImageId?: string;
56+
imageLayers?: string[];
57+
rootFs?: string[];
58+
imageName?: string;
59+
hashes?: string[];
60+
};
61+
rootFs?: string[];
62+
meta?: Meta;
63+
}
64+
65+
/**
66+
* Meta is an object that eventually gets passed to project.monitor.meta in Registry
67+
* and gets persisted in the database. It is currently passed unmodified straight
68+
* to the database.
69+
*
70+
* The project.monitor.meta table is an HSTORE (key-value strings) so we must ensure
71+
* that we pass exactly this type of data to avoid mistakes.
72+
*/
73+
export interface Meta extends Partial<Record<string, string>> {
74+
platform?: string;
75+
}
76+
77+
export function extractFactsFromDockerPluginResponse(
78+
pluginResponse: PluginResponse,
79+
): ExtractedFacts {
80+
const depGraph: depGraphLib.DepGraph = pluginResponse.scanResults[0].facts.find(
81+
(fact) => fact.type === 'depGraph',
82+
)?.data;
83+
84+
const manifestFiles:
85+
| ManifestFile[]
86+
| undefined = pluginResponse.scanResults[0].facts.find(
87+
(fact) => fact.type === 'imageManifestFiles',
88+
)?.data;
89+
90+
const hashes: string[] | undefined = pluginResponse.scanResults[0].facts.find(
91+
(fact) => fact.type === 'keyBinariesHashes',
92+
)?.data;
93+
94+
const imageLayers:
95+
| string[]
96+
| undefined = pluginResponse.scanResults[0].facts.find(
97+
(fact) => fact.type === 'imageLayers',
98+
)?.data;
99+
100+
const rootFs: string[] | undefined = pluginResponse.scanResults[0].facts.find(
101+
(fact) => fact.type === 'rootFs',
102+
)?.data;
103+
104+
const imageId: string | undefined = pluginResponse.scanResults[0].facts.find(
105+
(fact) => fact.type === 'imageId',
106+
)?.data;
107+
108+
const imageOsReleasePrettyName:
109+
| string
110+
| undefined = pluginResponse.scanResults[0].facts.find(
111+
(fact) => fact.type === 'imageOsReleasePrettyName',
112+
)?.data;
113+
114+
const platform = pluginResponse.scanResults[0].identity.args?.platform;
115+
116+
return {
117+
depGraph,
118+
manifestFiles,
119+
hashes,
120+
imageLayers,
121+
rootFs,
122+
imageId,
123+
imageOsReleasePrettyName,
124+
platform,
125+
};
126+
}
127+
128+
export function buildDockerPropertiesOnDepTree(
129+
depTree: depGraphLib.legacy.DepTree,
130+
dockerPluginFacts: ExtractedFacts,
131+
image: string,
132+
): DependencyTree {
133+
const {
134+
hashes,
135+
imageLayers,
136+
rootFs,
137+
imageId,
138+
imageOsReleasePrettyName,
139+
platform,
140+
} = dockerPluginFacts;
141+
142+
const mutatedDepTree = depTree as DependencyTree;
143+
mutatedDepTree.docker = {
144+
hashes,
145+
imageLayers,
146+
rootFs,
147+
dockerImageId: imageId,
148+
imageName: image,
149+
};
150+
151+
mutatedDepTree.dockerImageId = imageId || '';
152+
if (mutatedDepTree.targetOS) {
153+
mutatedDepTree.targetOS.prettyName = imageOsReleasePrettyName || '';
154+
}
155+
156+
if (!mutatedDepTree.meta) {
157+
mutatedDepTree.meta = {};
158+
}
159+
mutatedDepTree.meta.platform = platform;
160+
161+
return mutatedDepTree;
162+
}
163+
164+
/**
165+
* Produces a DependencyTree (DepsDiscoveryResult) for every ScanResult
166+
* that contains a dependency graph. ScanResults with other data are ignored
167+
* because the data cannot be resolved to a DepTree.
168+
*/
169+
export async function getApplicationDependencyTrees(
170+
applicationScanResults: ScanResult[],
171+
): Promise<DependencyTree[]> {
172+
const dependencyTrees: DependencyTree[] = [];
173+
174+
for (const scanResult of applicationScanResults) {
175+
const appDepGraph: depGraphLib.DepGraph | undefined = scanResult.facts.find(
176+
(fact) => fact.type === 'depGraph',
177+
)?.data;
178+
179+
// Skip this ScanResult if we could not read a dependency graph.
180+
// Some ScanResults like Java will not contain a graph but instead a list of hashes.
181+
// These are not supported by the current API.
182+
if (appDepGraph === undefined) {
183+
continue;
184+
}
185+
186+
const appDepTree = await depGraphLib.legacy.graphToDepTree(
187+
appDepGraph,
188+
appDepGraph.pkgManager.name,
189+
);
190+
191+
if (!appDepTree.name || !appDepTree.type) {
192+
continue;
193+
}
194+
195+
const testedFiles: string[] | undefined = scanResult.facts.find(
196+
(fact) => fact.type === 'testedFiles',
197+
)?.data;
198+
199+
dependencyTrees.push({
200+
name: appDepTree.name,
201+
version: appDepTree.version,
202+
type: appDepTree.type,
203+
dependencies: appDepTree.dependencies,
204+
targetFile: scanResult.identity.targetFile,
205+
testedFiles,
206+
});
207+
}
208+
209+
return dependencyTrees;
210+
}

src/scanner/images/index.ts

Lines changed: 82 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
import { unlink } from 'fs';
2-
import * as plugin from 'snyk-docker-plugin';
2+
import { PluginResponse, scan } from 'snyk-docker-plugin';
3+
import { DepGraph, legacy } from '@snyk/dep-graph';
34

45
import logger = require('../../common/logger');
56
import { pull as skopeoCopy, getDestinationForImage } from './skopeo';
67
import { IPullableImage, IScanImage } from './types';
7-
import { IStaticAnalysisOptions, StaticAnalysisImageType, IScanResult, IPluginOptions } from '../types';
8+
import { IScanResult } from '../types';
9+
import {
10+
buildDockerPropertiesOnDepTree,
11+
DependencyTree,
12+
extractFactsFromDockerPluginResponse,
13+
LegacyPluginResponse,
14+
} from './docker-plugin-shim';
815

916
export async function pullImages(images: IPullableImage[]): Promise<IPullableImage[]> {
1017
const pulledImages: IPullableImage[] = [];
@@ -65,43 +72,34 @@ export function getImageParts(imageWithTag: string) : {imageName: string, imageT
6572
};
6673
}
6774

68-
// Exported for testing
69-
export function constructStaticAnalysisOptions(
70-
fileSystemPath: string,
71-
): IStaticAnalysisOptions {
72-
return {
73-
imagePath: fileSystemPath,
74-
imageType: StaticAnalysisImageType.DockerArchive,
75-
};
76-
}
77-
7875
export async function scanImages(images: IPullableImage[]): Promise<IScanResult[]> {
7976
const scannedImages: IScanResult[] = [];
8077

81-
const dockerfile = undefined;
82-
8378
for (const { imageName, fileSystemPath, imageWithDigest } of images) {
8479
try {
85-
const staticAnalysisOptions = constructStaticAnalysisOptions(fileSystemPath);
86-
const options: IPluginOptions = {
87-
staticAnalysisOptions,
88-
experimental: true,
89-
};
80+
const shouldIncludeAppVulns = false;
81+
const dockerArchivePath = `docker-archive:${fileSystemPath}`;
9082

91-
const result = await plugin.inspect(imageName, dockerfile, options);
83+
const pluginResponse = await scan({
84+
path: dockerArchivePath,
85+
imageNameAndTag: imageName,
86+
'app-vulns': shouldIncludeAppVulns,
87+
});
9288

93-
if (!result || !result.package || !result.package.dependencies) {
89+
if (
90+
!pluginResponse ||
91+
!Array.isArray(pluginResponse.scanResults) ||
92+
pluginResponse.scanResults.length === 0
93+
) {
9494
throw Error('Unexpected empty result from docker-plugin');
9595
}
9696

97+
const depTree = await getDependencyTreeFromPluginResponse(pluginResponse, imageName);
98+
9799
const imageParts = getImageParts(imageName);
98100
const imageDigest = imageWithDigest && getImageParts(imageWithDigest).imageDigest;
99101

100-
result.imageMetadata = {
101-
image: imageParts.imageName,
102-
imageTag: imageParts.imageTag,
103-
imageDigest,
104-
};
102+
const result: LegacyPluginResponse = getLegacyPluginResponse(depTree, imageParts, imageDigest);
105103

106104
scannedImages.push({
107105
image: imageParts.imageName,
@@ -116,3 +114,61 @@ export async function scanImages(images: IPullableImage[]): Promise<IScanResult[
116114

117115
return scannedImages;
118116
}
117+
118+
function getLegacyPluginResponse(
119+
depTree: DependencyTree,
120+
imageParts: { imageName: string; imageTag: string; imageDigest: string },
121+
imageDigest: string | undefined,
122+
): LegacyPluginResponse {
123+
return {
124+
package: depTree,
125+
manifestFiles: [],
126+
plugin: {
127+
name: 'snyk-docker-plugin',
128+
imageLayers: depTree.docker?.imageLayers || [],
129+
dockerImageId:
130+
depTree.dockerImageId || depTree.docker?.dockerImageId || '',
131+
packageManager: depTree.type,
132+
runtime: undefined,
133+
},
134+
imageMetadata: {
135+
image: imageParts.imageName,
136+
imageTag: imageParts.imageTag,
137+
imageDigest,
138+
},
139+
hashes: depTree.docker?.hashes || [],
140+
};
141+
}
142+
143+
/**
144+
* Converts from the new plugin format back to the old DependencyTree format.
145+
* May throw if the expected data is missing.
146+
*/
147+
async function getDependencyTreeFromPluginResponse(
148+
pluginResponse: PluginResponse,
149+
imageName: string,
150+
): Promise<DependencyTree> {
151+
const osDepGraph:
152+
| DepGraph
153+
| undefined = pluginResponse.scanResults[0].facts.find(
154+
(fact) => fact.type === 'depGraph',
155+
)?.data;
156+
157+
if (!osDepGraph) {
158+
throw new Error('Missing dependency graph');
159+
}
160+
161+
const depTree = await legacy.graphToDepTree(
162+
osDepGraph,
163+
osDepGraph.pkgManager.name,
164+
);
165+
const osScanResultFacts = extractFactsFromDockerPluginResponse(
166+
pluginResponse,
167+
);
168+
const dockerDepTree = buildDockerPropertiesOnDepTree(
169+
depTree,
170+
osScanResultFacts,
171+
imageName,
172+
);
173+
return dockerDepTree;
174+
}

src/scanner/types.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,8 @@
1+
import { LegacyPluginResponse } from './images/docker-plugin-shim';
2+
13
export interface IScanResult {
24
image: string;
35
imageWithDigest?: string;
46
imageWithTag: string;
5-
pluginResult: any;
6-
}
7-
8-
export enum StaticAnalysisImageType {
9-
DockerArchive = 'docker-archive',
10-
}
11-
12-
export interface IPluginOptions {
13-
staticAnalysisOptions: IStaticAnalysisOptions;
14-
experimental: boolean;
15-
}
16-
17-
export interface IStaticAnalysisOptions {
18-
imagePath: string;
19-
imageType: StaticAnalysisImageType;
7+
pluginResult: LegacyPluginResponse;
208
}

0 commit comments

Comments
 (0)